github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/rpc/query/query.go (about) 1 package query 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "runtime" 10 "strings" 11 "sync/atomic" 12 13 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" 14 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/debug" 15 ) 16 17 // Params are used during calls to the RPC. 18 type Params []interface{} 19 20 // Payload is used to make calls to the RPC. 21 type Payload struct { 22 Headers map[string]string `json:"headers,omitempty"` 23 Method string `json:"method"` 24 Params `json:"params"` 25 } 26 27 type rpcResponse[T any] struct { 28 Result T `json:"result"` 29 Error *eip1474Error `json:"error"` 30 } 31 32 type eip1474Error struct { 33 Code int `json:"code"` 34 Message string `json:"message"` 35 } 36 37 var rpcCounter uint32 38 39 type rpcPayload struct { 40 Jsonrpc string `json:"jsonrpc"` 41 Method string `json:"method"` 42 Params `json:"params"` 43 ID int `json:"id"` 44 } 45 46 // BatchPayload is a wrapper around Payload type that allows us 47 // to associate a name (Key) to given request. 48 type BatchPayload struct { 49 Key string 50 *Payload 51 } 52 53 // Query returns a single result for given method and params. 54 func Query[T any](chain string, method string, params Params) (*T, error) { 55 url := config.GetChain(chain).RpcProvider 56 return QueryUrl[T](url, method, params) 57 } 58 59 // QueryUrl is just like Query, but it does not resolve chain to RPC provider URL 60 func QueryUrl[T any](url string, method string, params Params) (*T, error) { 61 return QueryWithHeaders[T](url, map[string]string{}, method, params) 62 } 63 64 // QueryWithHeaders returns a single result for a given method and params. 65 func QueryWithHeaders[T any](url string, headers map[string]string, method string, params Params) (*T, error) { 66 payloadToSend := rpcPayload{ 67 Jsonrpc: "2.0", 68 Method: method, 69 Params: params, 70 ID: int(uint32(atomic.AddUint32(&rpcCounter, 1))), 71 } 72 73 debug.DebugCurl(rpcDebug{url: url, payload: payloadToSend, headers: headers}) 74 75 if plBytes, err := json.Marshal(payloadToSend); err != nil { 76 return nil, err 77 } else { 78 body := bytes.NewReader(plBytes) 79 if request, err := http.NewRequest("POST", url, body); err != nil { 80 return nil, err 81 } else { 82 request.Header.Set("Content-Type", "application/json") 83 for key, value := range headers { 84 request.Header.Set(key, value) 85 } 86 87 client := &http.Client{} 88 if response, err := client.Do(request); err != nil { 89 return nil, err 90 } else if response.StatusCode != 200 { 91 return nil, fmt.Errorf("%s: %d", response.Status, response.StatusCode) 92 } else { 93 defer response.Body.Close() 94 95 if theBytes, err := io.ReadAll(response.Body); err != nil { 96 return nil, err 97 } else { 98 var result rpcResponse[T] 99 if err = json.Unmarshal(theBytes, &result); err != nil { 100 return nil, err 101 } else { 102 if result.Error != nil { 103 return nil, fmt.Errorf("%d: %s", result.Error.Code, result.Error.Message) 104 } 105 return &result.Result, nil 106 } 107 } 108 } 109 } 110 } 111 } 112 113 // QueryBatch batches requests to the node. Returned values are stored in map, with the same keys as defined 114 // in `batchPayload` (this way we don't have to operate on array indices) 115 func QueryBatch[T any](chain string, batchPayload []BatchPayload) (map[string]*T, error) { 116 return QueryBatchWithHeaders[T](chain, map[string]string{}, batchPayload) 117 } 118 119 func QueryBatchWithHeaders[T any](chain string, headers map[string]string, batchPayload []BatchPayload) (map[string]*T, error) { 120 keys := make([]string, 0, len(batchPayload)) 121 payloads := make([]Payload, 0, len(batchPayload)) 122 for _, bpl := range batchPayload { 123 keys = append(keys, bpl.Key) 124 payloads = append(payloads, *bpl.Payload) 125 } 126 127 url := config.GetChain(chain).RpcProvider 128 payloadToSend := make([]rpcPayload, 0, len(payloads)) 129 130 for _, payload := range payloads { 131 theLoad := rpcPayload{ 132 Jsonrpc: "2.0", 133 Method: payload.Method, 134 Params: payload.Params, 135 ID: int(atomic.AddUint32(&rpcCounter, 1)), 136 } 137 debug.DebugCurl(rpcDebug{ 138 url: url, 139 payload: theLoad, 140 headers: headers, 141 }) 142 payloadToSend = append(payloadToSend, theLoad) 143 } 144 145 plBytes, err := json.Marshal(payloadToSend) 146 if err != nil { 147 return nil, err 148 } 149 150 var result []rpcResponse[T] 151 body := bytes.NewReader(plBytes) 152 if response, err := http.Post(url, "application/json", body); err != nil { 153 return nil, err 154 } else { 155 defer response.Body.Close() 156 if theBytes, err := io.ReadAll(response.Body); err != nil { 157 return nil, err 158 } else { 159 if err = json.Unmarshal(theBytes, &result); err != nil { 160 return nil, err 161 } 162 results := make(map[string]*T, len(batchPayload)) 163 for index, key := range keys { 164 results[key] = &result[index].Result 165 } 166 return results, err 167 } 168 } 169 } 170 171 func init() { 172 // We need to increase MaxIdleConnsPerHost, otherwise chifra will keep trying to open too 173 // many ports. It can lead to bind errors. 174 // The default value is too low, so Go closes ports too fast. In the meantime, chifra tries 175 // to get new ones and so it can run out of available ports. 176 // 177 // We change DefaultTransport as the whole codebase uses it. 178 http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = runtime.GOMAXPROCS(0) * 4 179 } 180 181 type rpcDebug struct { 182 payload rpcPayload 183 url string 184 headers map[string]string 185 } 186 187 func (c rpcDebug) Url() string { 188 return c.url 189 } 190 191 func (c rpcDebug) Body() string { 192 return `curl -X POST [{headers}] --data '[{payload}]' [{url}]` 193 } 194 195 func (c rpcDebug) Headers() string { 196 ret := `-H "Content-Type: application/json"` 197 for key, value := range c.headers { 198 ret += fmt.Sprintf(` -H "%s: %s"`, key, value) 199 } 200 return ret 201 } 202 203 func (c rpcDebug) Method() string { 204 return c.payload.Method 205 } 206 207 func (c rpcDebug) Payload() string { 208 bytes, _ := json.MarshalIndent(c.payload, "", "") 209 payloadStr := strings.Replace(string(bytes), "\n", " ", -1) 210 return payloadStr 211 }