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