git.gammaspectra.live/P2Pool/consensus/v3@v3.8.0/monero/client/rpc/client.go (about) 1 package rpc 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "git.gammaspectra.live/P2Pool/consensus/v3/utils" 8 "io" 9 "net/http" 10 "net/url" 11 ) 12 13 const ( 14 // endpointJSONRPC is the common endpoint used for all the RPC calls 15 // that make use of epee's JSONRPC invocation format for requests and 16 // responses. 17 // 18 endpointJSONRPC = "/json_rpc" 19 20 // versionJSONRPC is the version of the JSONRPC format. 21 // 22 versionJSONRPC = "2.0" 23 ) 24 25 // Client is a wrapper over a plain HTTP client providing methods that 26 // correspond to all RPC invocations to a `monerod` daemon, including 27 // restricted and non-restricted ones. 28 type Client struct { 29 // http is the underlying http client that takes care of sending 30 // requests and receiving the responses. 31 // 32 // To provide your own, make use of `WithHTTPClient` when instantiating 33 // the client via the `NewClient` constructor. 34 // 35 http *http.Client 36 37 // address is the address of the monerod instance serving the RPC 38 // endpoints. 39 // 40 address *url.URL 41 } 42 43 // clientOptions is a set of options that can be overridden to tweak the 44 // client's behavior. 45 type clientOptions struct { 46 HTTPClient *http.Client 47 } 48 49 // ClientOption defines a functional option for overriding optional client 50 // configuration parameters. 51 type ClientOption func(o *clientOptions) 52 53 // WithHTTPClient is a functional option for providing a custom HTTP client to 54 // be used for the HTTP requests made to a monero daemon. 55 func WithHTTPClient(v *http.Client) func(o *clientOptions) { 56 return func(o *clientOptions) { 57 o.HTTPClient = v 58 } 59 } 60 61 // NewClient instantiates a new Client that is able to communicate with 62 // monerod's RPC endpoints. 63 // 64 // The `address` might be either restricted (typically <ip>:18089) or not 65 // (typically <ip>:18081). 66 func NewClient(address string, opts ...ClientOption) (*Client, error) { 67 options := &clientOptions{} 68 69 for _, opt := range opts { 70 opt(options) 71 } 72 73 if options.HTTPClient == nil { 74 options.HTTPClient = http.DefaultClient 75 } 76 77 parsedAddress, err := url.Parse(address) 78 if err != nil { 79 return nil, fmt.Errorf("url parse: %w", err) 80 } 81 82 return &Client{ 83 address: parsedAddress, 84 http: options.HTTPClient, 85 }, nil 86 } 87 88 // ResponseEnvelope wraps all responses from the RPC server. 89 type ResponseEnvelope struct { 90 ID string `json:"id"` 91 JSONRPC string `json:"jsonrpc"` 92 Result interface{} `json:"result,omitempty"` 93 Error struct { 94 Code int `json:"code"` 95 Message string `json:"message"` 96 } `json:"error,omitempty"` 97 } 98 99 // RequestEnvelope wraps all requests made to the RPC server. 100 type RequestEnvelope struct { 101 ID string `json:"id"` 102 JSONRPC string `json:"jsonrpc"` 103 Method string `json:"method"` 104 Params interface{} `json:"params,omitempty"` 105 } 106 107 // RawBinaryRequest makes requests to any endpoints, not assuming any particular format. 108 func (c *Client) RawBinaryRequest(ctx context.Context, endpoint string, body io.Reader) (io.ReadCloser, error) { 109 address := *c.address 110 address.Path = endpoint 111 112 req, err := http.NewRequestWithContext(ctx, "POST", address.String(), body) 113 if err != nil { 114 return nil, fmt.Errorf("new req '%s': %w", address.String(), err) 115 } 116 117 req.Header.Add("Content-Type", "application/octet-stream") 118 119 resp, err := c.http.Do(req) 120 if err != nil { 121 return nil, fmt.Errorf("do: %w", err) 122 } 123 124 if resp.StatusCode < 200 || resp.StatusCode > 299 { 125 defer resp.Body.Close() 126 return nil, fmt.Errorf("non-2xx status code: %d", resp.StatusCode) 127 } 128 129 return resp.Body, nil 130 } 131 132 // RawRequest makes requests to any endpoints, not assuming any particular format except of response is JSON. 133 func (c *Client) RawRequest(ctx context.Context, endpoint string, params interface{}, response interface{}) error { 134 address := *c.address 135 address.Path = endpoint 136 137 var body io.Reader 138 139 if params != nil { 140 b, err := utils.MarshalJSON(params) 141 if err != nil { 142 return fmt.Errorf("marshal: %w", err) 143 } 144 145 body = bytes.NewReader(b) 146 } 147 148 req, err := http.NewRequestWithContext(ctx, "GET", address.String(), body) 149 if err != nil { 150 return fmt.Errorf("new req '%s': %w", address.String(), err) 151 } 152 153 req.Header.Add("Content-Type", "application/json") 154 155 if err := c.submitRequest(req, response); err != nil { 156 return fmt.Errorf("submit request: %w", err) 157 } 158 159 return nil 160 } 161 162 // JSONRPC issues a request for a particular method under the JSONRPC endpoint 163 // with the proper envolope for its requests and unwrapping of results for 164 // responses. 165 func (c *Client) JSONRPC(ctx context.Context, method string, params interface{}, response interface{}) error { 166 address := *c.address 167 address.Path = endpointJSONRPC 168 169 b, err := utils.MarshalJSON(&RequestEnvelope{ 170 ID: "0", 171 JSONRPC: versionJSONRPC, 172 Method: method, 173 Params: params, 174 }) 175 if err != nil { 176 return fmt.Errorf("marshal: %w", err) 177 } 178 179 req, err := http.NewRequestWithContext(ctx, "GET", address.String(), bytes.NewReader(b)) 180 if err != nil { 181 return fmt.Errorf("new req '%s': %w", address.String(), err) 182 } 183 184 req.Header.Add("Content-Type", "application/json") 185 186 rpcResponseBody := &ResponseEnvelope{ 187 Result: response, 188 } 189 190 if err := c.submitRequest(req, rpcResponseBody); err != nil { 191 return fmt.Errorf("submit request: %w", err) 192 } 193 194 if rpcResponseBody.Error.Code != 0 || rpcResponseBody.Error.Message != "" { 195 return fmt.Errorf("rpc error: code=%d message=%s", 196 rpcResponseBody.Error.Code, 197 rpcResponseBody.Error.Message, 198 ) 199 } 200 201 return nil 202 } 203 204 // submitRequest performs any generic HTTP request to the monero node targeted 205 // by this client making no assumptions about a particular endpoint. 206 func (c *Client) submitRequest(req *http.Request, response interface{}) error { 207 resp, err := c.http.Do(req) 208 if err != nil { 209 return fmt.Errorf("do: %w", err) 210 } 211 212 defer resp.Body.Close() 213 214 if resp.StatusCode < 200 || resp.StatusCode > 299 { 215 return fmt.Errorf("non-2xx status code: %d", resp.StatusCode) 216 } 217 218 if err := utils.NewJSONDecoder(resp.Body).Decode(response); err != nil { 219 return fmt.Errorf("decode: %w", err) 220 } 221 222 return nil 223 }