github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/client.go (about) 1 package rpcclient 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net" 10 "net/http" 11 "net/url" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/nspcc-dev/neo-go/pkg/config/netmode" 17 "github.com/nspcc-dev/neo-go/pkg/neorpc" 18 "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" 19 "github.com/nspcc-dev/neo-go/pkg/util" 20 ) 21 22 const ( 23 defaultDialTimeout = 4 * time.Second 24 defaultRequestTimeout = 4 * time.Second 25 ) 26 27 // Client represents the middleman for executing JSON RPC calls 28 // to remote NEO RPC nodes. Client is thread-safe and can be used from 29 // multiple goroutines. 30 type Client struct { 31 cli *http.Client 32 endpoint *url.URL 33 ctx context.Context 34 // ctxCancel is a cancel function aimed to send closing signal to the users of 35 // ctx. 36 ctxCancel func() 37 opts Options 38 requestF func(*neorpc.Request) (*neorpc.Response, error) 39 40 // reader is an Invoker that has no signers and uses current state, 41 // it's used to implement various getters. It'll be removed eventually, 42 // but for now it keeps Client's API compatibility. 43 reader *invoker.Invoker 44 45 cacheLock sync.RWMutex 46 // cache stores RPC node related information the client is bound to. 47 // cache is mostly filled in during Init(), but can also be updated 48 // during regular Client lifecycle. 49 cache cache 50 51 latestReqID atomic.Uint64 52 // getNextRequestID returns an ID to be used for the subsequent request creation. 53 // It is defined on Client, so that our testing code can override this method 54 // for the sake of more predictable request IDs generation behavior. 55 getNextRequestID func() uint64 56 } 57 58 // Options defines options for the RPC client. 59 // All values are optional. If any duration is not specified, 60 // a default of 4 seconds will be used. 61 type Options struct { 62 // Cert is a client-side certificate, it doesn't work at the moment along 63 // with the other two options below. 64 Cert string 65 Key string 66 CACert string 67 DialTimeout time.Duration 68 RequestTimeout time.Duration 69 // Limit total number of connections per host. No limit by default. 70 MaxConnsPerHost int 71 } 72 73 // cache stores cache values for the RPC client methods. 74 type cache struct { 75 initDone bool 76 network netmode.Magic 77 stateRootInHeader bool 78 nativeHashes map[string]util.Uint160 79 } 80 81 // New returns a new Client ready to use. You should call Init method to 82 // initialize stateroot setting for the network the client is operating on if 83 // you plan using GetBlock*. 84 func New(ctx context.Context, endpoint string, opts Options) (*Client, error) { 85 cl := new(Client) 86 err := initClient(ctx, cl, endpoint, opts) 87 if err != nil { 88 return nil, err 89 } 90 return cl, nil 91 } 92 93 func initClient(ctx context.Context, cl *Client, endpoint string, opts Options) error { 94 url, err := url.Parse(endpoint) 95 if err != nil { 96 return err 97 } 98 99 if opts.DialTimeout <= 0 { 100 opts.DialTimeout = defaultDialTimeout 101 } 102 103 if opts.RequestTimeout <= 0 { 104 opts.RequestTimeout = defaultRequestTimeout 105 } 106 107 httpClient := &http.Client{ 108 Transport: &http.Transport{ 109 DialContext: (&net.Dialer{ 110 Timeout: opts.DialTimeout, 111 }).DialContext, 112 MaxConnsPerHost: opts.MaxConnsPerHost, 113 }, 114 Timeout: opts.RequestTimeout, 115 } 116 117 // TODO(@antdm): Enable SSL. 118 // if opts.Cert != "" && opts.Key != "" { 119 // } 120 121 cancelCtx, cancel := context.WithCancel(ctx) 122 cl.ctx = cancelCtx 123 cl.ctxCancel = cancel 124 cl.cli = httpClient 125 cl.endpoint = url 126 cl.cache = cache{ 127 nativeHashes: make(map[string]util.Uint160), 128 } 129 cl.latestReqID = atomic.Uint64{} 130 cl.getNextRequestID = (cl).getRequestID 131 cl.opts = opts 132 cl.requestF = cl.makeHTTPRequest 133 cl.reader = invoker.New(cl, nil) 134 return nil 135 } 136 137 func (c *Client) getRequestID() uint64 { 138 return c.latestReqID.Add(1) 139 } 140 141 // Init sets magic of the network client connected to, stateRootInHeader option 142 // and native NEO, GAS and Policy contracts scripthashes. This method should be 143 // called before any header- or block-related requests in order to deserialize 144 // responses properly. 145 func (c *Client) Init() error { 146 version, err := c.GetVersion() 147 if err != nil { 148 return fmt.Errorf("failed to get network magic: %w", err) 149 } 150 natives, err := c.GetNativeContracts() 151 if err != nil { 152 return fmt.Errorf("failed to get native contracts: %w", err) 153 } 154 155 c.cacheLock.Lock() 156 defer c.cacheLock.Unlock() 157 158 c.cache.network = version.Protocol.Network 159 c.cache.stateRootInHeader = version.Protocol.StateRootInHeader 160 for _, ctr := range natives { 161 c.cache.nativeHashes[ctr.Manifest.Name] = ctr.Hash 162 } 163 164 c.cache.initDone = true 165 return nil 166 } 167 168 // Close closes unused underlying networks connections. 169 func (c *Client) Close() { 170 c.ctxCancel() 171 c.cli.CloseIdleConnections() 172 } 173 174 func (c *Client) performRequest(method string, p []any, v any) error { 175 if p == nil { 176 p = []any{} // neo-project/neo-modules#742 177 } 178 var r = neorpc.Request{ 179 JSONRPC: neorpc.JSONRPCVersion, 180 Method: method, 181 Params: p, 182 ID: c.getNextRequestID(), 183 } 184 185 raw, err := c.requestF(&r) 186 187 if raw != nil && raw.Error != nil { 188 return raw.Error 189 } else if err != nil { 190 return err 191 } else if raw == nil || raw.Result == nil { 192 return errors.New("no result returned") 193 } 194 return json.Unmarshal(raw.Result, v) 195 } 196 197 func (c *Client) makeHTTPRequest(r *neorpc.Request) (*neorpc.Response, error) { 198 var ( 199 buf = new(bytes.Buffer) 200 raw = new(neorpc.Response) 201 ) 202 203 if err := json.NewEncoder(buf).Encode(r); err != nil { 204 return nil, err 205 } 206 207 req, err := http.NewRequest("POST", c.endpoint.String(), buf) 208 if err != nil { 209 return nil, err 210 } 211 resp, err := c.cli.Do(req) 212 if err != nil { 213 return nil, err 214 } 215 defer resp.Body.Close() 216 217 // The node might send us a proper JSON anyway, so look there first and if 218 // it parses, it has more relevant data than HTTP error code. 219 err = json.NewDecoder(resp.Body).Decode(raw) 220 if err != nil { 221 if resp.StatusCode != http.StatusOK { 222 err = fmt.Errorf("HTTP %d/%s", resp.StatusCode, http.StatusText(resp.StatusCode)) 223 } else { 224 err = fmt.Errorf("JSON decoding: %w", err) 225 } 226 } 227 if err != nil { 228 return nil, err 229 } 230 return raw, nil 231 } 232 233 // Ping attempts to create a connection to the endpoint 234 // and returns an error if there is any. 235 func (c *Client) Ping() error { 236 conn, err := net.DialTimeout("tcp", c.endpoint.Host, defaultDialTimeout) 237 if err != nil { 238 return err 239 } 240 _ = conn.Close() 241 return nil 242 } 243 244 // Context returns client instance context. 245 func (c *Client) Context() context.Context { 246 return c.ctx 247 } 248 249 // Endpoint returns the client endpoint. 250 func (c *Client) Endpoint() string { 251 return c.endpoint.String() 252 }