github.com/lmittmann/w3@v0.20.0/client.go (about)

     1  /*
     2  Package w3 is your toolbelt for integrating with Ethereum in Go. Closely linked
     3  to [go-ethereum], it provides an ergonomic wrapper for working with RPC, ABI's,
     4  and the EVM.
     5  
     6  [go-ethereum]: https://github.com/ethereum/go-ethereum
     7  */
     8  package w3
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"reflect"
    14  	"strings"
    15  
    16  	"github.com/ethereum/go-ethereum/rpc"
    17  	"github.com/lmittmann/w3/w3types"
    18  	"golang.org/x/time/rate"
    19  )
    20  
    21  // Client represents a connection to an RPC endpoint.
    22  type Client struct {
    23  	client *rpc.Client
    24  
    25  	// rate limiter
    26  	rl         *rate.Limiter
    27  	rlCostFunc func(methods []string) (cost int)
    28  }
    29  
    30  // NewClient returns a new Client given an rpc.Client client.
    31  func NewClient(client *rpc.Client, opts ...Option) *Client {
    32  	if client == nil {
    33  		panic("w3: client is nil")
    34  	}
    35  	c := &Client{client: client}
    36  	for _, opt := range opts {
    37  		if opt == nil {
    38  			continue
    39  		}
    40  		opt(c)
    41  	}
    42  	return c
    43  }
    44  
    45  // Dial returns a new Client connected to the URL rawurl. An error is returned
    46  // if the connection establishment fails.
    47  //
    48  // The supported URL schemes are "http", "https", "ws" and "wss". If rawurl is a
    49  // file name with no URL scheme, a local IPC socket connection is established.
    50  func Dial(rawurl string, opts ...Option) (*Client, error) {
    51  	client, err := rpc.Dial(rawurl)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return NewClient(client, opts...), nil
    56  }
    57  
    58  // MustDial is like [Dial] but panics if the connection establishment fails.
    59  func MustDial(rawurl string, opts ...Option) *Client {
    60  	client, err := Dial(rawurl, opts...)
    61  	if err != nil {
    62  		panic(fmt.Sprintf("w3: %s", err))
    63  	}
    64  	return client
    65  }
    66  
    67  // Close the RPC connection and cancel all in-flight requests.
    68  //
    69  // Close implements the [io.Closer] interface.
    70  func (c *Client) Close() error {
    71  	c.client.Close()
    72  	return nil
    73  }
    74  
    75  // CallCtx creates the final RPC request, sends it, and handles the RPC
    76  // response.
    77  //
    78  // An error is returned if RPC request creation, networking, or RPC response
    79  // handling fails.
    80  func (c *Client) CallCtx(ctx context.Context, calls ...w3types.RPCCaller) error {
    81  	// no requests = nothing to do
    82  	if len(calls) <= 0 {
    83  		return nil
    84  	}
    85  
    86  	// create requests
    87  	batchElems := make([]rpc.BatchElem, len(calls))
    88  	var err error
    89  	for i, req := range calls {
    90  		batchElems[i], err = req.CreateRequest()
    91  		if err != nil {
    92  			return err
    93  		}
    94  	}
    95  
    96  	// invoke rate limiter
    97  	if err := c.rateLimit(ctx, batchElems); err != nil {
    98  		return err
    99  	}
   100  
   101  	// do requests
   102  	if len(batchElems) > 1 {
   103  		// batch requests if >1 request
   104  		err = c.client.BatchCallContext(ctx, batchElems)
   105  		if err != nil {
   106  			return err
   107  		}
   108  	} else {
   109  		// non-batch requests if 1 request
   110  		batchElem := batchElems[0]
   111  		err = c.client.CallContext(ctx, batchElem.Result, batchElem.Method, batchElem.Args...)
   112  		if err != nil {
   113  			switch reflect.TypeOf(err).String() {
   114  			case "*rpc.jsonError":
   115  				batchElems[0].Error = err
   116  			default:
   117  				return err
   118  			}
   119  		}
   120  	}
   121  
   122  	// handle responses
   123  	var callErrs CallErrors
   124  	for i, req := range calls {
   125  		err = req.HandleResponse(batchElems[i])
   126  		if err != nil {
   127  			if callErrs == nil {
   128  				callErrs = make(CallErrors, len(calls))
   129  			}
   130  			callErrs[i] = err
   131  		}
   132  	}
   133  	if len(callErrs) > 0 {
   134  		return callErrs
   135  	}
   136  	return nil
   137  }
   138  
   139  // Call is like [Client.CallCtx] with ctx equal to context.Background().
   140  func (c *Client) Call(calls ...w3types.RPCCaller) error {
   141  	return c.CallCtx(context.Background(), calls...)
   142  }
   143  
   144  // SubscribeCtx creates a new subscription and returns a [rpc.ClientSubscription].
   145  func (c *Client) SubscribeCtx(ctx context.Context, s w3types.RPCSubscriber) (*rpc.ClientSubscription, error) {
   146  	namespace, ch, params, err := s.CreateRequest()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	return c.client.Subscribe(ctx, namespace, ch, params...)
   151  }
   152  
   153  // Subscribe is like [Client.SubscribeCtx] with ctx equal to context.Background().
   154  func (c *Client) Subscribe(s w3types.RPCSubscriber) (*rpc.ClientSubscription, error) {
   155  	return c.SubscribeCtx(context.Background(), s)
   156  }
   157  
   158  func (c *Client) rateLimit(ctx context.Context, batchElems []rpc.BatchElem) error {
   159  	if c.rl == nil {
   160  		return nil
   161  	}
   162  
   163  	if c.rlCostFunc == nil {
   164  		// limit requests
   165  		return c.rl.Wait(ctx)
   166  	}
   167  
   168  	// limit requests based on Compute Units (CUs)
   169  	methods := make([]string, len(batchElems))
   170  	for i, batchElem := range batchElems {
   171  		methods[i] = batchElem.Method
   172  	}
   173  	cost := c.rlCostFunc(methods)
   174  	return c.rl.WaitN(ctx, cost)
   175  }
   176  
   177  // CallErrors is an error type that contains the errors of multiple calls. The
   178  // length of the error slice is equal to the number of calls. Each error at a
   179  // given index corresponds to the call at the same index. An error is nil if the
   180  // corresponding call was successful.
   181  type CallErrors []error
   182  
   183  func (e CallErrors) Error() string {
   184  	if len(e) == 1 && e[0] != nil {
   185  		return fmt.Sprintf("w3: call failed: %s", e[0])
   186  	}
   187  
   188  	var errors []string
   189  	for i, err := range e {
   190  		if err == nil {
   191  			continue
   192  		}
   193  		errors = append(errors, fmt.Sprintf("call[%d]: %s", i, err))
   194  	}
   195  
   196  	var plr string
   197  	if len(errors) > 1 {
   198  		plr = "s"
   199  	}
   200  	return fmt.Sprintf("w3: %d call%s failed:\n%s", len(errors), plr, strings.Join(errors, "\n"))
   201  }
   202  
   203  func (e CallErrors) Is(target error) bool {
   204  	_, ok := target.(CallErrors)
   205  	return ok
   206  }
   207  
   208  // An Option configures a Client.
   209  type Option func(*Client)
   210  
   211  // WithRateLimiter sets the rate limiter for the client. Set the optional argument
   212  // costFunc to nil to limit the number of requests. Supply a costFunc to limit
   213  // the number of requests based on individual RPC calls for advanced rate
   214  // limiting by e.g. Compute Units (CUs). Note that only if len(methods) > 1, the
   215  // calls are sent in a batch request.
   216  func WithRateLimiter(rl *rate.Limiter, costFunc func(methods []string) (cost int)) Option {
   217  	return func(c *Client) {
   218  		c.rl = rl
   219  		c.rlCostFunc = costFunc
   220  	}
   221  }