github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/rpc/provider/covalent.go (about) 1 package provider 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "strings" 11 "time" 12 13 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base" 14 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" 15 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc" 16 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types" 17 "golang.org/x/time/rate" 18 ) 19 20 // const covalentFirstPage = 0 21 const covalentRequestsPerSecond = 5 22 const covalentBaseUrl = "https://api.covalenthq.com/v1/[{CHAIN}]/address/[{ADDRESS}]/transactions_v3/page/[{PAGE}]/" 23 24 var tbChainToCovalent = map[string]string{ 25 "mainnet": "eth-mainnet", 26 } 27 28 func covalentPrepareQuery(q *Query) (result *Query) { 29 result = q.Dup() 30 result.Resources = []string{"covalent"} 31 return 32 } 33 34 type CovalentProvider struct { 35 printProgress bool 36 conn *rpc.Connection 37 limiter *rate.Limiter 38 baseUrl string 39 chain string 40 apiKey string 41 } 42 43 func NewCovalentProvider(conn *rpc.Connection, chain string) (p *CovalentProvider, err error) { 44 apiKey := config.GetKey("covalent").ApiKey 45 if apiKey == "" { 46 err = errors.New("missing Covalent API key") 47 return 48 } 49 p = &CovalentProvider{ 50 conn: conn, 51 chain: chain, 52 apiKey: apiKey, 53 baseUrl: covalentBaseUrl, 54 } 55 p.printProgress = true 56 p.limiter = rate.NewLimiter(covalentRequestsPerSecond, covalentRequestsPerSecond) 57 58 return 59 } 60 61 func (p *CovalentProvider) PrintProgress() bool { 62 return p.printProgress 63 } 64 65 func (p *CovalentProvider) SetPrintProgress(print bool) { 66 p.printProgress = print 67 } 68 69 func (p *CovalentProvider) NewPaginator(query *Query) Paginator { 70 pageNumber := query.StartPage 71 return NewPageNumberPaginator(pageNumber, pageNumber, int(query.PerPage)) 72 } 73 74 func (p *CovalentProvider) TransactionsByAddress(ctx context.Context, query *Query, errorChan chan error) (txChan chan types.Slurp) { 75 txChan = make(chan types.Slurp, providerChannelBufferSize) 76 77 prepQuery := covalentPrepareQuery(query) 78 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 79 go func() { 80 defer close(txChan) 81 for { 82 select { 83 case <-ctx.Done(): 84 return 85 case item, ok := <-slurpedChan: 86 if !ok { 87 return 88 } 89 txChan <- *item.Transaction 90 } 91 } 92 }() 93 94 return 95 } 96 97 func (p *CovalentProvider) Appearances(ctx context.Context, query *Query, errorChan chan error) (appChan chan types.Appearance) { 98 appChan = make(chan types.Appearance, providerChannelBufferSize) 99 100 prepQuery := covalentPrepareQuery(query) 101 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 102 go func() { 103 defer close(appChan) 104 for { 105 select { 106 case <-ctx.Done(): 107 return 108 case item, ok := <-slurpedChan: 109 if !ok { 110 return 111 } 112 appChan <- *item.Appearance 113 } 114 } 115 }() 116 117 return 118 } 119 120 func (p *CovalentProvider) Count(ctx context.Context, query *Query, errorChan chan error) (monitorChan chan types.Monitor) { 121 prepQuery := covalentPrepareQuery(query) 122 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 123 return countSlurped(ctx, query, slurpedChan) 124 } 125 126 type covalentResponseBody struct { 127 Data covalentResponseData `json:"data"` 128 } 129 130 type covalentResponseData struct { 131 Items []covalentTransaction `json:"items"` 132 Links *covalentLinks `json:"links"` 133 } 134 135 type covalentTransaction struct { 136 BlockHeight *int `json:"block_height,omitempty"` 137 BlockHash *string `json:"block_hash,omitempty"` 138 TxHash *string `json:"tx_hash,omitempty"` 139 TxOffset *int `json:"tx_offset,omitempty"` 140 Successful *bool `json:"successful,omitempty"` 141 From *string `json:"from_address,omitempty"` 142 To *string `json:"to_address,omitempty"` 143 Value *base.Wei `json:"value,omitempty"` 144 GasSpent *int64 `json:"gas_spent,omitempty"` 145 GasPrice *int64 `json:"gas_price,omitempty"` 146 BlockSignedAt *time.Time `json:"block_signed_at,omitempty"` 147 } 148 149 func (c *covalentTransaction) Slurp() (s types.Slurp) { 150 to := "" 151 if c.To != nil { 152 to = *c.To 153 } 154 s = types.Slurp{ 155 BlockHash: base.HexToHash(*c.BlockHash), 156 BlockNumber: base.Blknum(*c.BlockHeight), 157 From: base.HexToAddress(*c.From), 158 Gas: base.Gas(*c.GasSpent), 159 IsError: !(*c.Successful), 160 Timestamp: base.Timestamp(c.BlockSignedAt.Unix()), 161 To: base.HexToAddress(to), 162 TransactionIndex: base.Txnum(*c.TxOffset), 163 Value: *c.Value, 164 } 165 166 return 167 } 168 169 func (c *covalentTransaction) Appearance(address base.Address) (a types.Appearance) { 170 return types.Appearance{ 171 Address: address, 172 BlockNumber: uint32(*c.BlockHeight), 173 TransactionIndex: uint32(*c.TxOffset), 174 } 175 } 176 177 type covalentLinks struct { 178 Prev string `json:"prev,omitempty"` 179 Next string `json:"next,omitempty"` 180 } 181 182 func (e *CovalentProvider) fetchData(ctx context.Context, address base.Address, paginator Paginator, _ string) (data []SlurpedPageItem, count int, err error) { 183 if e.baseUrl == "" { 184 e.baseUrl = covalentBaseUrl 185 } 186 187 pageNumber, ok := paginator.Page().(int) 188 if !ok { 189 err = errors.New("cannot get page number") 190 return 191 } 192 193 url, err := e.url(address, pageNumber) 194 if err != nil { 195 return 196 } 197 198 var response covalentResponseBody 199 request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 200 if err != nil { 201 return 202 } 203 request.SetBasicAuth(e.apiKey, "") 204 request.Header.Set("Content-Type", "application/json") 205 206 resp, err := http.DefaultClient.Do(request) 207 if err != nil { 208 return 209 } 210 if resp.StatusCode != http.StatusOK { 211 err = fmt.Errorf("covalent responded with: %s", resp.Status) 212 paginator.SetDone(true) 213 return 214 } 215 defer resp.Body.Close() 216 respBytes, err := io.ReadAll(resp.Body) 217 if err != nil { 218 return 219 } 220 if err = json.Unmarshal(respBytes, &response); err != nil { 221 return 222 } 223 224 data = make([]SlurpedPageItem, 0, len(response.Data.Items)) 225 for _, covalentTx := range response.Data.Items { 226 appearance := covalentTx.Appearance(address) 227 slurpedTx := covalentTx.Slurp() 228 data = append(data, SlurpedPageItem{ 229 Appearance: &appearance, 230 Transaction: &slurpedTx, 231 }) 232 } 233 // update paginator 234 paginator.SetDone(response.Data.Links.Next == "") 235 236 count = len(data) 237 return 238 } 239 240 func (p *CovalentProvider) url(address base.Address, pageNumber int) (url string, err error) { 241 covalentChain := tbChainToCovalent[p.chain] 242 if covalentChain == "" { 243 err = fmt.Errorf("cannot find covalent chain ID for chain %s", p.chain) 244 return 245 } 246 247 url = p.baseUrl 248 url = strings.Replace(url, "[{CHAIN}]", covalentChain, -1) 249 url = strings.Replace(url, "[{ADDRESS}]", address.Hex(), -1) 250 url = strings.Replace(url, "[{PAGE}]", fmt.Sprint(pageNumber), -1) 251 return 252 }