@@ -51,6 +51,23 @@ type EtherscanTransaction struct {
51
51
Confirmations string `json:"confirmations"`
52
52
}
53
53
54
+ type EtherscanTransferEvent struct {
55
+ BlockNumber string `json:"blockNumber"`
56
+ TimeStamp string `json:"timeStamp"`
57
+ TransactionHash string `json:"transactionHash"`
58
+ LogIndex string `json:"logIndex"`
59
+ From string `json:"from"`
60
+ To string `json:"to"`
61
+ Value string `json:"value"`
62
+ ContractAddress string `json:"address"`
63
+ TransactionIndex string `json:"transactionIndex"`
64
+ GasUsed string `json:"gasUsed"`
65
+ CumulativeGasUsed string `json:"cumulativeGasUsed"`
66
+ BlockHash string `json:"blockHash"`
67
+ Data string `json:"data"`
68
+ Topics []string `json:"topics"`
69
+ }
70
+
54
71
func NewEtherscanClient (apiKey string , logger * zap.Logger ) * EtherscanClient {
55
72
return & EtherscanClient {
56
73
apiKey : apiKey ,
@@ -127,7 +144,7 @@ func (c *EtherscanClient) GetTransactions(ctx context.Context, startBlock, endBl
127
144
var rawResp json.RawMessage
128
145
if err := c .makeRequestWithRetry (ctx , params , & rawResp ); err != nil {
129
146
if strings .Contains (err .Error (), "No transactions found" ) {
130
- c .logger .Info ("No transactions found for address" ,
147
+ c .logger .Debug ("No transactions found for address" ,
131
148
zap .String ("address" , address ))
132
149
continue // Skip this address and continue with the next one
133
150
}
@@ -181,12 +198,80 @@ func (c *EtherscanClient) GetTransactions(ctx context.Context, startBlock, endBl
181
198
allTransactions = append (allTransactions , transactions ... )
182
199
}
183
200
184
- c .logger .Info ("Finished processing all addresses" ,
201
+ // Fetch Transfer events for the NFT contracts
202
+ transferEvents , err := c .GetTransferEvents (ctx , startBlock , endBlock )
203
+
204
+ if err != nil {
205
+ c .logger .Info (err .Error ())
206
+ if strings .Contains (err .Error (), "No records found" ) {
207
+ c .logger .Debug ("No records found for contract" )
208
+ } else {
209
+ c .logger .Error ("Failed to fetch transfer events" , zap .Error (err ))
210
+ }
211
+ } else {
212
+ c .logger .Debug ("Transfer events found" , zap .Int ("count" , len (transferEvents )))
213
+ c .logger .Debug ("Transfer events" , zap .Any ("events" , transferEvents ))
214
+ for _ , event := range transferEvents {
215
+ transaction := ConvertTransferEventToTransaction (event )
216
+ c .logger .Debug ("Transaction" , zap .Any ("transaction" , transaction ))
217
+ allTransactions = append (allTransactions , transaction )
218
+ }
219
+ }
220
+
221
+ c .logger .Debug ("Finished processing all transactions" ,
185
222
zap .Int ("totalTransactions" , len (allTransactions )))
186
223
187
224
return allTransactions , nil
188
225
}
189
226
227
+ func (c * EtherscanClient ) GetTransferEvents (ctx context.Context , startBlock , endBlock int64 ) ([]EtherscanTransferEvent , error ) {
228
+ contractAddresses := []string {
229
+ "0x050dc61dFB867E0fE3Cf2948362b6c0F3fAF790b" , // OpenSea contract address
230
+ // Add other relevant contract addresses here
231
+ }
232
+
233
+ var allEvents []EtherscanTransferEvent
234
+
235
+ for _ , contract := range contractAddresses {
236
+ params := map [string ]string {
237
+ "module" : "logs" ,
238
+ "action" : "getLogs" ,
239
+ "fromBlock" : strconv .FormatInt (startBlock , 10 ),
240
+ "toBlock" : strconv .FormatInt (endBlock , 10 ),
241
+ "address" : contract ,
242
+ "topic0" : "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" , // Transfer event signature
243
+ "apikey" : c .apiKey ,
244
+ }
245
+
246
+ var rawResp json.RawMessage
247
+ if err := c .makeRequestWithRetry (ctx , params , & rawResp ); err != nil {
248
+ if strings .Contains (err .Error (), "No records found" ) {
249
+ c .logger .Debug ("No records found for contract" , zap .String ("contract" , contract ))
250
+ continue
251
+ } else {
252
+ c .logger .Error ("Failed to fetch transfer events" ,
253
+ zap .Error (err ),
254
+ zap .String ("contract" , contract ))
255
+ continue
256
+ }
257
+ }
258
+ c .logger .Debug ("Raw response" , zap .String ("rawResponse" , string (rawResp )))
259
+
260
+ var events []EtherscanTransferEvent
261
+ if err := json .Unmarshal (rawResp , & events ); err != nil {
262
+ c .logger .Error ("Failed to unmarshal transfer events" ,
263
+ zap .Error (err ),
264
+ zap .String ("rawResponse" , string (rawResp )),
265
+ zap .String ("contract" , contract ))
266
+ continue
267
+ }
268
+
269
+ allEvents = append (allEvents , events ... )
270
+ }
271
+
272
+ return allEvents , nil
273
+ }
274
+
190
275
func (c * EtherscanClient ) makeRequest (ctx context.Context , params map [string ]string , result interface {}) error {
191
276
if err := c .limiter .Wait (ctx ); err != nil {
192
277
return fmt .Errorf ("rate limiter wait: %w" , err )
@@ -258,3 +343,50 @@ func (c *EtherscanClient) makeRequestWithRetry(ctx context.Context, params map[s
258
343
259
344
return backoff .Retry (operation , b )
260
345
}
346
+
347
+ // ConvertTransferEventToTransaction converts an EtherscanTransferEvent to an EtherscanTransaction
348
+ func ConvertTransferEventToTransaction (event EtherscanTransferEvent ) EtherscanTransaction {
349
+ // event.BlockNumber is a string hex value, need to convert to int but still as string
350
+ blockNumber , _ := strconv .ParseInt (event .BlockNumber [2 :], 16 , 64 )
351
+ timeStamp , _ := strconv .ParseInt (event .TimeStamp [2 :], 16 , 64 ) // Convert hex to int64
352
+ nonce , _ := strconv .ParseInt (event .LogIndex [2 :], 16 , 64 ) // Convert hex to int64
353
+ gas , _ := strconv .ParseInt (event .GasUsed [2 :], 16 , 64 ) // Convert hex to int64
354
+
355
+ // Construct the input data for safeTransferFrom
356
+ from := "0x" + event .Topics [1 ][26 :] // Extracting the 'from' address from topics
357
+ to := "0x" + event .Topics [2 ][26 :] // Extracting the 'to' address from topics
358
+ tokenId := event .Topics [3 ] // Token ID in hex
359
+
360
+ // Pad addresses and tokenId to 32 bytes (64 hex characters)
361
+ paddedFrom := fmt .Sprintf ("%064s" , strings .TrimPrefix (from , "0x" ))
362
+ paddedTo := fmt .Sprintf ("%064s" , strings .TrimPrefix (to , "0x" ))
363
+ paddedTokenId := fmt .Sprintf ("%064s" , strings .TrimPrefix (tokenId , "0x" ))
364
+
365
+ // safeTransferFrom(address,address,uint256) method signature
366
+ methodSignature := "0x42842e0e"
367
+ inputData := methodSignature + paddedFrom + paddedTo + paddedTokenId
368
+
369
+ return EtherscanTransaction {
370
+ BlockNumber : strconv .FormatInt (blockNumber , 10 ),
371
+ TimeStamp : strconv .FormatInt (timeStamp , 10 ), // Convert int64 to string
372
+ Hash : event .TransactionHash ,
373
+ From : from ,
374
+ To : "0x050dc61dfb867e0fe3cf2948362b6c0f3faf790b" ,
375
+ Value : "0" , // safeTransferFrom typically has no value transfer
376
+ ContractAddress : event .ContractAddress ,
377
+ TransactionIndex : event .TransactionIndex ,
378
+ Gas : strconv .FormatInt (gas , 10 ),
379
+ GasUsed : strconv .FormatInt (gas , 10 ),
380
+ GasPrice : "0" ,
381
+ CumulativeGasUsed : "0" ,
382
+ Nonce : strconv .FormatInt (nonce , 10 ),
383
+ Confirmations : "0" ,
384
+ Input : inputData , // Set the input data to mimic safeTransferFrom
385
+ }
386
+ }
387
+
388
+ // Helper function to parse hex string to uint64
389
+ func parseHexToUint64 (hexStr string ) uint64 {
390
+ value , _ := strconv .ParseUint (hexStr [2 :], 16 , 64 )
391
+ return value
392
+ }
0 commit comments