169 lines
5.5 KiB
Go
169 lines
5.5 KiB
Go
package chains
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/txtracker/config"
|
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
)
|
|
|
|
const requestTimeout = 30 * time.Second
|
|
|
|
var (
|
|
ErrChainNotSupported = errors.New("chain id not supported")
|
|
ErrTransactionNotFound = errors.New("transaction not found")
|
|
)
|
|
|
|
type TxDetail struct {
|
|
// Signer is the address that signed the transaction, encoded in the chain's native format.
|
|
Signer string
|
|
// Timestamp indicates the time at which the transaction was confirmed.
|
|
Timestamp time.Time
|
|
// NativeTxHash contains the transaction hash, encoded in the chain's native format.
|
|
NativeTxHash string
|
|
}
|
|
|
|
var tickers = struct {
|
|
ankr *time.Ticker
|
|
arbitrum *time.Ticker
|
|
avalanche *time.Ticker
|
|
bsc *time.Ticker
|
|
celo *time.Ticker
|
|
eth *time.Ticker
|
|
fantom *time.Ticker
|
|
optimism *time.Ticker
|
|
polygon *time.Ticker
|
|
solana *time.Ticker
|
|
terra *time.Ticker
|
|
}{}
|
|
|
|
func Initialize(cfg *config.RpcProviderSettings) {
|
|
|
|
// f converts "requests per minute" into the associated time.Duration
|
|
f := func(requestsPerMinute uint16) time.Duration {
|
|
|
|
division := float64(time.Minute) / float64(time.Duration(requestsPerMinute))
|
|
roundedUp := math.Ceil(division)
|
|
|
|
return time.Duration(roundedUp)
|
|
}
|
|
|
|
tickers.ankr = time.NewTicker(f(cfg.AnkrRequestsPerMinute))
|
|
tickers.terra = time.NewTicker(f(cfg.TerraRequestsPerMinute))
|
|
|
|
// these adapters send 2 requests per txHash
|
|
tickers.arbitrum = time.NewTicker(f(cfg.ArbitrumRequestsPerMinute) / 2)
|
|
tickers.avalanche = time.NewTicker(f(cfg.AvalancheRequestsPerMinute) / 2)
|
|
tickers.bsc = time.NewTicker(f(cfg.BscRequestsPerMinute) / 2)
|
|
tickers.eth = time.NewTicker(f(cfg.EthRequestsPerMinute) / 2)
|
|
tickers.fantom = time.NewTicker(f(cfg.FantomRequestsPerMinute) / 2)
|
|
tickers.celo = time.NewTicker(f(cfg.CeloRequestsPerMinute) / 2)
|
|
tickers.optimism = time.NewTicker(f(cfg.OptimismRequestsPerMinute) / 2)
|
|
tickers.polygon = time.NewTicker(f(cfg.PolygonRequestsPerMinute) / 2)
|
|
tickers.solana = time.NewTicker(f(cfg.SolanaRequestsPerMinute / 2))
|
|
}
|
|
|
|
func FetchTx(
|
|
ctx context.Context,
|
|
cfg *config.RpcProviderSettings,
|
|
chainId vaa.ChainID,
|
|
txHash string,
|
|
) (*TxDetail, error) {
|
|
|
|
var fetchFunc func(context.Context, *config.RpcProviderSettings, string) (*TxDetail, error)
|
|
var rateLimiter time.Ticker
|
|
|
|
// decide which RPC/API service to use based on chain ID
|
|
switch chainId {
|
|
case vaa.ChainIDSolana:
|
|
fetchFunc = fetchSolanaTx
|
|
rateLimiter = *tickers.solana
|
|
case vaa.ChainIDTerra:
|
|
fetchFunc = fetchTerraTx
|
|
rateLimiter = *tickers.terra
|
|
case vaa.ChainIDCelo:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.CeloBaseUrl, cfg.CeloApiKey)
|
|
}
|
|
rateLimiter = *tickers.celo
|
|
case vaa.ChainIDEthereum:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.EthBaseUrl, cfg.EthApiKey)
|
|
}
|
|
rateLimiter = *tickers.eth
|
|
case vaa.ChainIDBSC:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.BscBaseUrl, cfg.BscApiKey)
|
|
}
|
|
rateLimiter = *tickers.bsc
|
|
case vaa.ChainIDPolygon:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.PolygonBaseUrl, cfg.PolygonApiKey)
|
|
}
|
|
rateLimiter = *tickers.polygon
|
|
case vaa.ChainIDFantom:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.FantomBaseUrl, cfg.FantomApiKey)
|
|
}
|
|
rateLimiter = *tickers.fantom
|
|
case vaa.ChainIDArbitrum:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.ArbitrumBaseUrl, cfg.ArbitrumApiKey)
|
|
}
|
|
rateLimiter = *tickers.arbitrum
|
|
case vaa.ChainIDOptimism:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.OptimismBaseUrl, cfg.OptimismApiKey)
|
|
}
|
|
rateLimiter = *tickers.optimism
|
|
case vaa.ChainIDAvalanche:
|
|
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
|
|
return fetchEthTx(ctx, txHash, cfg.AvalancheBaseUrl, cfg.AvalancheApiKey)
|
|
}
|
|
rateLimiter = *tickers.avalanche
|
|
default:
|
|
return nil, ErrChainNotSupported
|
|
}
|
|
|
|
// wait for rate limit - fail fast if context was cancelled
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-rateLimiter.C:
|
|
}
|
|
|
|
// get transaction details from the RPC/API service
|
|
subContext, cancelFunc := context.WithTimeout(ctx, requestTimeout)
|
|
defer cancelFunc()
|
|
txDetail, err := fetchFunc(subContext, cfg, txHash)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve tx information: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|