[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:
agodnic 2023-03-27 16:14:12 -03:00 committed by GitHub
parent e1298f54ee
commit 02ffff93bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 142 additions and 24 deletions

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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

74
tx-tracker/chains/celo.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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"`

View File

@ -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),
)
}