diff --git a/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml b/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml index 34961fd9..9c2fbd59 100644 --- a/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml +++ b/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml @@ -41,6 +41,12 @@ spec: value: {{ .ANKR_BASE_URL }} - name: ANKR_REQUESTS_PER_MINUTE value: {{ .ANKR_REQUESTS_PER_MINUTE }} + - name: CELO_API_KEY + value: {{ .CELO_API_KEY }} + - name: CELO_BASE_URL + value: {{ .CELO_BASE_URL }} + - name: CELO_REQUESTS_PER_MINUTE + value: "{{ .CELO_REQUESTS_PER_MINUTE }}" - name: SOLANA_BASE_URL value: {{ .SOLANA_BASE_URL }} - name: SOLANA_REQUESTS_PER_MINUTE diff --git a/deploy/tx-tracker/tx-tracker-service.yaml b/deploy/tx-tracker/tx-tracker-service.yaml index 918a91d5..22e4a8f0 100644 --- a/deploy/tx-tracker/tx-tracker-service.yaml +++ b/deploy/tx-tracker/tx-tracker-service.yaml @@ -71,18 +71,26 @@ spec: value: {{ .VAA_PAYLOAD_PARSER_URL }} - name: VAA_PAYLOAD_PARSER_TIMEOUT value: "{{ .VAA_PAYLOAD_PARSER_TIMEOUT }}" + - name: ANKR_API_KEY + value: {{ .ANKR_API_KEY }} - name: ANKR_BASE_URL value: {{ .ANKR_BASE_URL }} - name: ANKR_REQUESTS_PER_MINUTE - value: "30" + value: "{{ .ANKR_REQUESTS_PER_MINUTE }}" + - name: CELO_API_KEY + value: {{ .CELO_API_KEY }} + - name: CELO_BASE_URL + value: {{ .CELO_BASE_URL }} + - name: CELO_REQUESTS_PER_MINUTE + value: "{{ .CELO_REQUESTS_PER_MINUTE }}" - name: SOLANA_BASE_URL value: {{ .SOLANA_BASE_URL }} - name: SOLANA_REQUESTS_PER_MINUTE - value: "8" + value: "{{ .SOLANA_REQUESTS_PER_MINUTE }}" - name: TERRA_BASE_URL value: {{ .TERRA_BASE_URL }} - name: TERRA_REQUESTS_PER_MINUTE - value: "6" + value: "{{ .TERRA_REQUESTS_PER_MINUTE }}" resources: limits: memory: {{ .RESOURCES_LIMITS_MEMORY }} diff --git a/tx-tracker/.env.example b/tx-tracker/.env.example index 337aa6fa..7568d246 100644 --- a/tx-tracker/.env.example +++ b/tx-tracker/.env.example @@ -15,10 +15,14 @@ BULK_SIZE=500 ANKR_BASE_URL=https://rpc.ankr.com/multichain ANKR_API_KEY= -ANKR_REQUESTS_PER_MINUTE= +ANKR_REQUESTS_PER_MINUTE=30 + +CELO_BASE_URL=https://rpc.ankr.com/celo +CELO_API_KEY= +CELO_REQUESTS_PER_MINUTE=6 SOLANA_BASE_URL=https://api.mainnet-beta.solana.com -SOLANA_REQUESTS_PER_MINUTE= +SOLANA_REQUESTS_PER_MINUTE=8 TERRA_BASE_URL=https://lcd.terra.dev -TERRA_REQUESTS_PER_MINUTE= \ No newline at end of file +TERRA_REQUESTS_PER_MINUTE=6 \ No newline at end of file diff --git a/tx-tracker/chains/ankr.go b/tx-tracker/chains/ankr.go index efc31ad7..a4231d4d 100644 --- a/tx-tracker/chains/ankr.go +++ b/tx-tracker/chains/ankr.go @@ -3,9 +3,7 @@ package chains import ( "context" "fmt" - "strconv" "strings" - "time" "github.com/ethereum/go-ethereum/rpc" @@ -86,15 +84,9 @@ func ankrFetchTx( } // parse transaction timestamp - var timestamp time.Time - { - hexDigits := strings.Replace(reply.Transactions[0].Timestamp, "0x", "", 1) - hexDigits = strings.Replace(hexDigits, "0X", "", 1) - epoch, err := strconv.ParseInt(hexDigits, 16, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse transaction timestamp: %w", err) - } - timestamp = time.Unix(epoch, 0).UTC() + timestamp, err := timestampFromHex(reply.Transactions[0].Timestamp) + if err != nil { + return nil, fmt.Errorf("failed to parse transaction timestamp: %w", err) } // build results and return diff --git a/tx-tracker/chains/celo.go b/tx-tracker/chains/celo.go new file mode 100644 index 00000000..ff4d5d20 --- /dev/null +++ b/tx-tracker/chains/celo.go @@ -0,0 +1,74 @@ +package chains + +import ( + "context" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/wormhole-foundation/wormhole-explorer/txtracker/config" +) + +type ethGetTransactionByHashResponse struct { + BlockHash string `json:"blockHash"` + BlockNumber string `json:"blockNumber"` + From string `json:"from"` + To string `json:"to"` +} + +type ethGetBlockByHashResponse struct { + Timestamp string `json:"timestamp"` + Number string `json:"number"` +} + +func fetchCeloTx( + ctx context.Context, + cfg *config.RpcProviderSettings, + txHash string, +) (*TxDetail, error) { + + // build RPC URL + url := cfg.CeloBaseUrl + if cfg.CeloApiKey != "" { + url += "/" + cfg.CeloApiKey + } + + // initialize RPC client + client, err := rpc.DialContext(ctx, url) + if err != nil { + return nil, fmt.Errorf("failed to initialize RPC client: %w", err) + } + defer client.Close() + + // query transaction data + var txReply ethGetTransactionByHashResponse + err = client.CallContext(ctx, &txReply, "eth_getTransactionByHash", "0x"+txHash) + if err != nil { + return nil, fmt.Errorf("failed to get tx by hash: %w", err) + } + + // query block data + blkParams := []interface{}{ + txReply.BlockHash, // tx hash + false, // include transactions? + } + var blkReply ethGetBlockByHashResponse + err = client.CallContext(ctx, &blkReply, "eth_getBlockByHash", blkParams...) + if err != nil { + return nil, fmt.Errorf("failed to get block by hash: %w", err) + } + + // parse transaction timestamp + timestamp, err := timestampFromHex(blkReply.Timestamp) + if err != nil { + return nil, fmt.Errorf("failed to parse block timestamp: %w", err) + } + + // build results and return + txDetail := &TxDetail{ + Signer: strings.ToLower(txReply.From), + Timestamp: timestamp, + NativeTxHash: fmt.Sprintf("0x%s", strings.ToLower(txHash)), + } + return txDetail, nil +} diff --git a/tx-tracker/chains/tx.go b/tx-tracker/chains/tx.go index fdf980e0..07c5c5cc 100644 --- a/tx-tracker/chains/tx.go +++ b/tx-tracker/chains/tx.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "math" + "strconv" + "strings" "time" "github.com/wormhole-foundation/wormhole-explorer/txtracker/config" @@ -29,6 +31,7 @@ type TxDetail struct { var tickers = struct { ankr *time.Ticker + celo *time.Ticker solana *time.Ticker terra *time.Ticker }{} @@ -47,7 +50,8 @@ func Initialize(cfg *config.RpcProviderSettings) { tickers.ankr = time.NewTicker(f(cfg.AnkrRequestsPerMinute)) tickers.terra = time.NewTicker(f(cfg.TerraRequestsPerMinute)) - // the Solana adapter sends 2 requests per txHash + // these adapters send 2 requests per txHash + tickers.celo = time.NewTicker(f(cfg.AnkrRequestsPerMinute) / 2) tickers.solana = time.NewTicker(f(cfg.SolanaRequestsPerMinute / 2)) } @@ -69,6 +73,9 @@ func FetchTx( case vaa.ChainIDTerra: fetchFunc = fetchTerraTx rateLimiter = *tickers.terra + case vaa.ChainIDCelo: + fetchFunc = fetchCeloTx + rateLimiter = *tickers.celo // most EVM-compatible chains use the same RPC service case vaa.ChainIDEthereum, vaa.ChainIDBSC, @@ -101,3 +108,21 @@ func FetchTx( return txDetail, nil } + +// timestampFromHex converts a hex timestamp into a `time.Time` value. +func timestampFromHex(s string) (time.Time, error) { + + // remove the leading "0x" or "0X" from the hex string + hexDigits := strings.Replace(s, "0x", "", 1) + hexDigits = strings.Replace(hexDigits, "0X", "", 1) + + // parse the hex digits into an integer + epoch, err := strconv.ParseInt(hexDigits, 16, 64) + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse hex timestamp: %w", err) + } + + // convert the unix epoch into a `time.Time` value + timestamp := time.Unix(epoch, 0).UTC() + return timestamp, nil +} diff --git a/tx-tracker/cmd/fetchone/main.go b/tx-tracker/cmd/fetchone/main.go index 6fa7aa01..d9f63f68 100644 --- a/tx-tracker/cmd/fetchone/main.go +++ b/tx-tracker/cmd/fetchone/main.go @@ -37,6 +37,6 @@ func main() { } // print tx details - log.Printf("tx detail: sender=%s receiver=%s timestamp=%s", + log.Printf("tx detail: sender=%s nativeTxHash=%s timestamp=%s", txDetail.Signer, txDetail.NativeTxHash, txDetail.Timestamp) } diff --git a/tx-tracker/cmd/service/main.go b/tx-tracker/cmd/service/main.go index 7329fe13..6eba5c83 100644 --- a/tx-tracker/cmd/service/main.go +++ b/tx-tracker/cmd/service/main.go @@ -34,14 +34,19 @@ func main() { log.Fatal("Error loading config: ", err) } - // initialize rate limiters - chains.Initialize(&cfg.RpcProviderSettings) - // build logger logger := logger.New("wormhole-explorer-tx-tracker", logger.WithLevel(cfg.LogLevel)) logger.Info("Starting wormhole-explorer-tx-tracker ...") + // initialize rate limiters + chains.Initialize(&cfg.RpcProviderSettings) + if cfg.AnkrApiKey != "" { + logger.Info("Ankr API key enabled") + } else { + logger.Info("Ankr API key disabled") + } + // initialize the database client cli, err := mongo.Connect(rootCtx, options.Client().ApplyURI(cfg.MongodbUri)) if err != nil { diff --git a/tx-tracker/config/structs.go b/tx-tracker/config/structs.go index 74d9a841..116edea2 100644 --- a/tx-tracker/config/structs.go +++ b/tx-tracker/config/structs.go @@ -52,6 +52,10 @@ type RpcProviderSettings struct { AnkrApiKey string `split_words:"true" required:"false"` AnkrRequestsPerMinute uint16 `split_words:"true" required:"true"` + CeloBaseUrl string `split_words:"true" required:"true"` + CeloApiKey string `split_words:"true" required:"false"` + CeloRequestsPerMinute uint16 `split_words:"true" required:"true"` + SolanaBaseUrl string `split_words:"true" required:"true"` SolanaRequestsPerMinute uint16 `split_words:"true" required:"true"` diff --git a/tx-tracker/consumer/consumer.go b/tx-tracker/consumer/consumer.go index e0ad9e1c..dd78e4ab 100644 --- a/tx-tracker/consumer/consumer.go +++ b/tx-tracker/consumer/consumer.go @@ -32,7 +32,7 @@ const ( const ( numRetries = 2 - retryDelay = 5 * time.Second + retryDelay = 10 * time.Second ) const AppIdPortalTokenBridge = "PORTAL_TOKEN_BRIDGE" @@ -141,7 +141,7 @@ func (c *Consumer) Start(ctx context.Context) { zap.Error(err), ) } else { - c.logger.Debug("Successfuly updated source transaction details in the database", + c.logger.Debug("Updated source transaction details in the database", zap.String("id", event.ID), ) }