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  }