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  }