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 }