git.gammaspectra.live/P2Pool/consensus@v0.0.0-20240403173234-a039820b20c9/monero/client/zmq/zmq.go (about) 1 package zmq 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "git.gammaspectra.live/P2Pool/consensus/utils" 8 "slices" 9 "strings" 10 11 "github.com/go-zeromq/zmq4" 12 ) 13 14 type Client struct { 15 endpoint string 16 topics []Topic 17 sub zmq4.Socket 18 } 19 20 // NewClient instantiates a new client that will receive monerod's zmq events. 21 // 22 // - `topics` is a list of fully-formed zmq topic to subscribe to 23 // 24 // - `endpoint` is the full address where monerod has been configured to 25 // publish the messages to, including the network schama. for instance, 26 // considering that monerod has been started with 27 // 28 // monerod --zmq-pub tcp://127.0.0.1:18085 29 // 30 // `endpoint` should be 'tcp://127.0.0.1:18085'. 31 func NewClient(endpoint string, topics ...Topic) *Client { 32 return &Client{ 33 endpoint: endpoint, 34 topics: topics, 35 } 36 } 37 38 // Stream provides channels where instances of the desired topic object are 39 // sent to. 40 type Stream struct { 41 FullChainMainC func(*FullChainMain) 42 FullTxPoolAddC func([]FullTxPoolAdd) 43 FullMinerDataC func(*FullMinerData) 44 MinimalChainMainC func(*MinimalChainMain) 45 MinimalTxPoolAddC func([]TxMempoolData) 46 } 47 48 // Listen listens for a list of topics pre-configured for this client (via NewClient). 49 func (c *Client) Listen(ctx context.Context, fullChainMain func(chainMain *FullChainMain), fullTxPoolAdd func(txs []FullTxPoolAdd), fullMinerData func(main *FullMinerData), minimalChainMain func(chainMain *MinimalChainMain), minimalTxPoolAdd func(txs []TxMempoolData)) error { 50 if err := c.listen(ctx, c.topics...); err != nil { 51 return fmt.Errorf("listen on '%s': %w", strings.Join(func() (r []string) { 52 for _, s := range c.topics { 53 r = append(r, string(s)) 54 } 55 return r 56 }(), ", "), err) 57 } 58 59 stream := &Stream{ 60 FullChainMainC: fullChainMain, 61 FullTxPoolAddC: fullTxPoolAdd, 62 FullMinerDataC: fullMinerData, 63 MinimalChainMainC: minimalChainMain, 64 MinimalTxPoolAddC: minimalTxPoolAdd, 65 } 66 67 if err := c.loop(stream); err != nil { 68 return fmt.Errorf("loop: %w", err) 69 } 70 71 return nil 72 } 73 74 // Close closes any established connection, if any. 75 func (c *Client) Close() error { 76 if c.sub == nil { 77 return nil 78 } 79 80 return c.sub.Close() 81 } 82 83 func (c *Client) listen(ctx context.Context, topics ...Topic) error { 84 c.sub = zmq4.NewSub(ctx) 85 86 err := c.sub.Dial(c.endpoint) 87 if err != nil { 88 return fmt.Errorf("dial '%s': %w", c.endpoint, err) 89 } 90 91 for _, topic := range topics { 92 err = c.sub.SetOption(zmq4.OptionSubscribe, string(topic)) 93 if err != nil { 94 return fmt.Errorf("subscribe: %w", err) 95 } 96 } 97 98 return nil 99 } 100 101 func (c *Client) loop(stream *Stream) error { 102 for { 103 msg, err := c.sub.Recv() 104 if err != nil { 105 return fmt.Errorf("recv: %w", err) 106 } 107 108 for _, frame := range msg.Frames { 109 err := c.ingestFrameArray(stream, frame) 110 if err != nil { 111 return fmt.Errorf("consume frame: %w", err) 112 } 113 } 114 } 115 } 116 117 func (c *Client) ingestFrameArray(stream *Stream, frame []byte) error { 118 topic, gson, err := jsonFromFrame(frame) 119 if err != nil { 120 return fmt.Errorf("json from frame: %w", err) 121 } 122 123 if slices.Index(c.topics, topic) == -1 { 124 return fmt.Errorf("topic '%s' doesn't match "+ 125 "expected any of '%s'", topic, strings.Join(func() (r []string) { 126 for _, s := range c.topics { 127 r = append(r, string(s)) 128 } 129 return r 130 }(), ", ")) 131 } 132 133 switch Topic(topic) { 134 case TopicFullChainMain: 135 return c.transmitFullChainMain(stream, gson) 136 case TopicFullTxPoolAdd: 137 return c.transmitFullTxPoolAdd(stream, gson) 138 case TopicFullMinerData: 139 return c.transmitFullMinerData(stream, gson) 140 case TopicMinimalChainMain: 141 return c.transmitMinimalChainMain(stream, gson) 142 case TopicMinimalTxPoolAdd: 143 return c.transmitMinimalTxPoolAdd(stream, gson) 144 default: 145 return fmt.Errorf("unhandled topic '%s'", topic) 146 } 147 } 148 149 func (c *Client) transmitFullChainMain(stream *Stream, gson []byte) error { 150 var arr []*FullChainMain 151 152 if err := utils.UnmarshalJSON(gson, &arr); err != nil { 153 return fmt.Errorf("unmarshal: %w", err) 154 } 155 for _, element := range arr { 156 stream.FullChainMainC(element) 157 } 158 159 return nil 160 } 161 162 func (c *Client) transmitFullTxPoolAdd(stream *Stream, gson []byte) error { 163 var arr []FullTxPoolAdd 164 165 if err := utils.UnmarshalJSON(gson, &arr); err != nil { 166 return fmt.Errorf("unmarshal: %w", err) 167 } 168 stream.FullTxPoolAddC(arr) 169 170 return nil 171 } 172 173 func (c *Client) transmitFullMinerData(stream *Stream, gson []byte) error { 174 element := &FullMinerData{} 175 176 if err := utils.UnmarshalJSON(gson, element); err != nil { 177 return fmt.Errorf("unmarshal: %w", err) 178 } 179 stream.FullMinerDataC(element) 180 return nil 181 } 182 183 func (c *Client) transmitMinimalChainMain(stream *Stream, gson []byte) error { 184 element := &MinimalChainMain{} 185 186 if err := utils.UnmarshalJSON(gson, element); err != nil { 187 return fmt.Errorf("unmarshal: %w", err) 188 } 189 stream.MinimalChainMainC(element) 190 return nil 191 } 192 193 func (c *Client) transmitMinimalTxPoolAdd(stream *Stream, gson []byte) error { 194 var arr []TxMempoolData 195 196 if err := utils.UnmarshalJSON(gson, &arr); err != nil { 197 return fmt.Errorf("unmarshal: %w", err) 198 } 199 stream.MinimalTxPoolAddC(arr) 200 201 return nil 202 } 203 204 func jsonFromFrame(frame []byte) (Topic, []byte, error) { 205 unknown := TopicUnknown 206 207 parts := bytes.SplitN(frame, []byte(":"), 2) 208 if len(parts) != 2 { 209 return unknown, nil, fmt.Errorf( 210 "malformed: expected 2 parts, got %d", len(parts)) 211 } 212 213 topic, gson := string(parts[0]), parts[1] 214 215 switch topic { 216 case string(TopicMinimalChainMain): 217 return TopicMinimalChainMain, gson, nil 218 case string(TopicFullChainMain): 219 return TopicFullChainMain, gson, nil 220 case string(TopicFullMinerData): 221 return TopicFullMinerData, gson, nil 222 case string(TopicFullTxPoolAdd): 223 return TopicFullTxPoolAdd, gson, nil 224 case string(TopicMinimalTxPoolAdd): 225 return TopicMinimalTxPoolAdd, gson, nil 226 } 227 228 return unknown, nil, fmt.Errorf("unknown topic '%s'", topic) 229 }