[TX-TRACKER] Add support for Celo (#209)
### Summary This pull request upgrades the tx-tracker service, adding support for transactions originated in the Celo blockchain. Tracking issue: #206
This commit is contained in:
parent
e1298f54ee
commit
02ffff93bf
|
@ -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
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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=
|
||||
TERRA_REQUESTS_PER_MINUTE=6
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue