github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/rpc/provider/alchemy.go (about) 1 package provider 2 3 import ( 4 "context" 5 "errors" 6 "strings" 7 8 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base" 9 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" 10 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc" 11 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc/query" 12 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types" 13 "golang.org/x/time/rate" 14 ) 15 16 const alchemyFirstPage = "" 17 const alchemyRequestsPerSecond = 5 // 330 Compute Units per second 18 // const alchemyMaxPerPage = 1000 19 const alchemyBaseUrl = "https://eth-mainnet.g.alchemy.com/v2/" 20 21 func alchemyPrepareQuery(q *Query) (result *Query, err error) { 22 result = q.Dup() 23 result.Resources = make([]string, 0, len(q.Resources)*2) 24 for _, resource := range q.Resources { 25 cat := alchemyCategory(resource) 26 if cat == "" { 27 // Ignore unknown categories, so the user can simply use 28 // `slurp --types all` 29 continue 30 } 31 result.Resources = append(result.Resources, cat+":to", cat+":from") 32 } 33 34 return 35 } 36 37 func alchemyCategory(requestType string) (category string) { 38 switch requestType { 39 case "ext": 40 category = "external" 41 case "int": 42 category = "internal" 43 case "token": 44 category = "erc20" 45 case "nfts": 46 category = "erc721" 47 case "1155": 48 category = "erc1155" 49 } 50 return 51 } 52 53 type AlchemyProvider struct { 54 printProgress bool 55 perPage int //nolint:unused 56 conn *rpc.Connection 57 limiter *rate.Limiter 58 baseUrl string 59 chain string 60 getTransactionAppearance func(hash string) (types.Appearance, error) 61 } 62 63 func NewAlchemyProvider(conn *rpc.Connection, chain string) (p *AlchemyProvider, err error) { 64 apiKey := config.GetKey("alchemy").ApiKey 65 if apiKey == "" { 66 err = errors.New("missing Alchemy API key") 67 return 68 } 69 70 p = &AlchemyProvider{ 71 conn: conn, 72 chain: chain, 73 baseUrl: alchemyBaseUrl + apiKey, 74 } 75 p.printProgress = true 76 p.limiter = rate.NewLimiter(alchemyRequestsPerSecond, alchemyRequestsPerSecond) 77 p.getTransactionAppearance = p.defaultGetTransactionAppearance 78 79 return 80 } 81 82 func (p *AlchemyProvider) PrintProgress() bool { 83 return p.printProgress 84 } 85 86 func (p *AlchemyProvider) SetPrintProgress(print bool) { 87 p.printProgress = print 88 } 89 90 func (p *AlchemyProvider) NewPaginator(query *Query) Paginator { 91 pageId := query.StartPageId 92 if pageId == "" { 93 pageId = alchemyFirstPage 94 } 95 96 return NewPageIdPaginator(pageId, pageId, int(query.PerPage)) 97 } 98 99 func (p *AlchemyProvider) TransactionsByAddress(ctx context.Context, query *Query, errorChan chan error) (txChan chan types.Slurp) { 100 txChan = make(chan types.Slurp, providerChannelBufferSize) 101 102 prepQuery, err := alchemyPrepareQuery(query) 103 if err != nil { 104 errorChan <- err 105 return 106 } 107 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 108 go func() { 109 defer close(txChan) 110 for { 111 select { 112 case <-ctx.Done(): 113 return 114 case item, ok := <-slurpedChan: 115 if !ok { 116 return 117 } 118 txChan <- *item.Transaction 119 } 120 } 121 }() 122 123 return 124 } 125 126 func (p *AlchemyProvider) Appearances(ctx context.Context, query *Query, errorChan chan error) (appChan chan types.Appearance) { 127 appChan = make(chan types.Appearance, providerChannelBufferSize) 128 129 prepQuery, err := alchemyPrepareQuery(query) 130 if err != nil { 131 errorChan <- err 132 return 133 } 134 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 135 go func() { 136 defer close(appChan) 137 for { 138 select { 139 case <-ctx.Done(): 140 return 141 case item, ok := <-slurpedChan: 142 if !ok { 143 return 144 } 145 appChan <- *item.Appearance 146 } 147 } 148 }() 149 150 return 151 } 152 153 func (p *AlchemyProvider) Count(ctx context.Context, query *Query, errorChan chan error) (monitorChan chan types.Monitor) { 154 prepQuery, err := alchemyPrepareQuery(query) 155 if err != nil { 156 errorChan <- err 157 return 158 } 159 slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData) 160 return countSlurped(ctx, query, slurpedChan) 161 } 162 163 type alchemyRequestParam struct { 164 ToBlock string `json:"toBlock,omitempty"` 165 ToAddress string `json:"toAddress,omitempty"` 166 FromAddress string `json:"fromAddress,omitempty"` 167 Category []string `json:"category,omitempty"` 168 PageKey string `json:"pageKey,omitempty"` 169 } 170 171 type alchemyResponseBody struct { 172 Transfers []AlchemyTx `json:"transfers"` 173 PageKey string `json:"pageKey"` 174 } 175 176 type AlchemyTx struct { 177 BlockNumber string `json:"blockNum"` 178 Hash string `json:"hash"` 179 From string `json:"from"` 180 To string `json:"to"` 181 } 182 183 func (tx *AlchemyTx) SimpleSlurp() (s types.Slurp, err error) { 184 s = types.Slurp{ 185 BlockNumber: base.MustParseBlknum(tx.BlockNumber), 186 Hash: base.HexToHash(tx.Hash), 187 From: base.HexToAddress(tx.From), 188 To: base.HexToAddress(tx.To), 189 } 190 191 return 192 } 193 194 func (e *AlchemyProvider) fetchData(ctx context.Context, address base.Address, paginator Paginator, categoryToken string) (data []SlurpedPageItem, count int, err error) { 195 pageKey, ok := paginator.Page().(string) 196 if !ok { 197 err = errors.New("cannot get page id") 198 return 199 } 200 201 // categoryToken has form of alchemyCategory[:from|to] 202 category := strings.Split(categoryToken, ":") 203 204 method := "alchemy_getAssetTransfers" 205 requestParam := alchemyRequestParam{ 206 ToBlock: "latest", 207 Category: []string{category[0]}, 208 PageKey: pageKey, 209 } 210 if len(category) > 1 && category[1] == "to" { 211 requestParam.ToAddress = address.Hex() 212 } else { 213 requestParam.FromAddress = address.Hex() 214 } 215 params := query.Params{requestParam} 216 217 var response *alchemyResponseBody 218 if response, err = query.QueryUrl[alchemyResponseBody](e.baseUrl, method, params); err != nil { 219 return 220 } 221 222 // log.Printf("Got: %+v\n", response) 223 224 data = make([]SlurpedPageItem, 0, len(response.Transfers)) 225 for _, alchemyTx := range response.Transfers { 226 app, err := e.getTransactionAppearance(alchemyTx.Hash) 227 if err != nil { 228 return []SlurpedPageItem{}, 0, err 229 } 230 tx, err := alchemyTx.SimpleSlurp() 231 if err != nil { 232 return []SlurpedPageItem{}, 0, err 233 } 234 tx.TransactionIndex = base.Txnum(app.TransactionIndex) 235 data = append(data, SlurpedPageItem{ 236 Appearance: &types.Appearance{ 237 TransactionIndex: app.TransactionIndex, 238 BlockNumber: app.BlockNumber, 239 Address: address, 240 }, 241 Transaction: &tx, 242 }) 243 } 244 // update paginator 245 _ = paginator.SetNextPage(response.PageKey) 246 paginator.SetDone(response.PageKey == "") 247 248 count = len(data) 249 return 250 } 251 252 func (p *AlchemyProvider) defaultGetTransactionAppearance(hash string) (types.Appearance, error) { 253 return p.conn.GetTransactionAppByHash(hash) 254 }