[1520] Retrieve the feeUSD from coingecko in tx-tracker [WIP] (#1541)
* start implementing retrieving the feeUSD from coingecko * add missing pricesApi in the initialization of tx-tracker service * add missing initialization * add missing cfg for coingecko properties * fix solana in url * replace coingecko api call with notional redis client * add native token list in commons * add decimal places * change feeUSD to Raw.GasPrice * add check for mainnet * replace with sdk const for more coupling * replace pricesApi with notionalCache * undo changes on pricesApi * undo more stuff * remove lines * remove unused timestamp * remove unused props * remove * fix indent * fix import * change * add cache configs * change token_address to native * fixes * fix BNB coingeckoID * fix polygon coingecko_id * change to lowercase * adjust type FeeDoc in operations endpoint * change Blast native token * pr review changes * code-review fixes * comment wormchain * rename var * add comment to wormchain gas token map entry
This commit is contained in:
parent
a55451968f
commit
6ab6824d82
|
@ -77,6 +77,9 @@ type AttributeDoc struct {
|
|||
|
||||
type FeeDoc struct {
|
||||
Fee string `bson:"fee" json:"fee"`
|
||||
RawFee map[string]any `bson:"rawFee" json:"rawFee"`
|
||||
GasTokenNotional string `bson:"gasTokenNotional" json:"gasTokenNotional"`
|
||||
FeeUSD string `bson:"feeUSD" json:"feeUSD"`
|
||||
}
|
||||
|
||||
// DestinationTx represents a destination transaction.
|
||||
|
|
|
@ -31,6 +31,8 @@ var (
|
|||
// NFT Bridge
|
||||
"0000000000000000000000000000000000000000000000000000000000000005": "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130",
|
||||
}
|
||||
|
||||
gasTokenList = GasTokenList()
|
||||
)
|
||||
|
||||
var allChainIDs = make(map[sdk.ChainID]bool)
|
||||
|
@ -399,3 +401,12 @@ func encodeBech32(hrp string, data []byte) (string, error) {
|
|||
|
||||
return bech32.Encode(hrp, aligned)
|
||||
}
|
||||
|
||||
func GetGasTokenMetadata(chainID sdk.ChainID) *TokenMetadata {
|
||||
for i := 0; i < len(gasTokenList); i++ {
|
||||
if gasTokenList[i].TokenChain == chainID {
|
||||
return &gasTokenList[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package domain
|
||||
|
||||
import sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
// manualMainnetTokenList returns a list of tokens that are not generated automatically.
|
||||
func manualMainnetTokenList() []TokenMetadata {
|
||||
return []TokenMetadata{
|
||||
|
@ -32,9 +34,62 @@ func manualMainnetTokenList() []TokenMetadata {
|
|||
// mainnetTokenList returns a list of all tokens on the mainnet.
|
||||
func mainnetTokenList() []TokenMetadata {
|
||||
res := append(generatedMainnetTokenList(), manualMainnetTokenList()...)
|
||||
res = append(res, GasTokenList()...)
|
||||
return append(res, unknownTokenList()...)
|
||||
}
|
||||
|
||||
// GasTokenList : gas tokens are the ones used to pay gas fees on the respective chains, they don't belong to a contract address.
|
||||
func GasTokenList() []TokenMetadata {
|
||||
const nativeTokenAddress = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
return []TokenMetadata{
|
||||
{TokenChain: sdk.ChainIDSolana, TokenAddress: nativeTokenAddress, Symbol: "SOL", CoingeckoID: "solana", Decimals: 9},
|
||||
{TokenChain: sdk.ChainIDEthereum, TokenAddress: nativeTokenAddress, Symbol: "ETH", CoingeckoID: "ethereum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDTerra, TokenAddress: nativeTokenAddress, Symbol: "LUNA", CoingeckoID: "terra-luna", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDBSC, TokenAddress: nativeTokenAddress, Symbol: "BNB", CoingeckoID: "binancecoin", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDPolygon, TokenAddress: nativeTokenAddress, Symbol: "MATIC", CoingeckoID: "matic-network", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDAvalanche, TokenAddress: nativeTokenAddress, Symbol: "AVAX", CoingeckoID: "avalanche-2", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDOasis, TokenAddress: nativeTokenAddress, Symbol: "ROSE", CoingeckoID: "oasis-network", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDAlgorand, TokenAddress: nativeTokenAddress, Symbol: "ALGO", CoingeckoID: "algorand", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDAurora, TokenAddress: nativeTokenAddress, Symbol: "AOA", CoingeckoID: "aurora", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDFantom, TokenAddress: nativeTokenAddress, Symbol: "FTM", CoingeckoID: "fantom", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDKarura, TokenAddress: nativeTokenAddress, Symbol: "KAR", CoingeckoID: "karura", Decimals: 12},
|
||||
{TokenChain: sdk.ChainIDAcala, TokenAddress: nativeTokenAddress, Symbol: "ACA", CoingeckoID: "acala", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDKlaytn, TokenAddress: nativeTokenAddress, Symbol: "KLAY", CoingeckoID: "klay-token", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDCelo, TokenAddress: nativeTokenAddress, Symbol: "CELO", CoingeckoID: "celo", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDNear, TokenAddress: nativeTokenAddress, Symbol: "NEAR", CoingeckoID: "near", Decimals: 24},
|
||||
{TokenChain: sdk.ChainIDMoonbeam, TokenAddress: nativeTokenAddress, Symbol: "GLMR", CoingeckoID: "moonbeam", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDTerra2, TokenAddress: nativeTokenAddress, Symbol: "LUNA", CoingeckoID: "terra-luna-2", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDInjective, TokenAddress: nativeTokenAddress, Symbol: "INJ", CoingeckoID: "injective-protocol", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDOsmosis, TokenAddress: nativeTokenAddress, Symbol: "OSMO", CoingeckoID: "osmosis", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDSui, TokenAddress: nativeTokenAddress, Symbol: "SUI", CoingeckoID: "sui", Decimals: 9},
|
||||
{TokenChain: sdk.ChainIDAptos, TokenAddress: nativeTokenAddress, Symbol: "APT", CoingeckoID: "aptos", Decimals: 8},
|
||||
{TokenChain: sdk.ChainIDArbitrum, TokenAddress: nativeTokenAddress, Symbol: "ARB", CoingeckoID: "arbitrum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDOptimism, TokenAddress: nativeTokenAddress, Symbol: "OP", CoingeckoID: "optimism", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDGnosis, TokenAddress: nativeTokenAddress, Symbol: "GNO", CoingeckoID: "gnosis", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDXpla, TokenAddress: nativeTokenAddress, Symbol: "XPLA", CoingeckoID: "xpla", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDBtc, TokenAddress: nativeTokenAddress, Symbol: "BTC", CoingeckoID: "bitcoin", Decimals: 8},
|
||||
{TokenChain: sdk.ChainIDBase, TokenAddress: nativeTokenAddress, Symbol: "ETH", CoingeckoID: "ethereum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDSei, TokenAddress: nativeTokenAddress, Symbol: "SEI", CoingeckoID: "sei-network", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDRootstock, TokenAddress: nativeTokenAddress, Symbol: "RSK", CoingeckoID: "rootstock", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDScroll, TokenAddress: nativeTokenAddress, Symbol: "ETH", CoingeckoID: "ethereum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDMantle, TokenAddress: nativeTokenAddress, Symbol: "MNT", CoingeckoID: "mantle", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDBlast, TokenAddress: nativeTokenAddress, Symbol: "ETH", CoingeckoID: "ethereum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDXLayer, TokenAddress: nativeTokenAddress, Symbol: "XLYR", CoingeckoID: "xlayer", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDLinea, TokenAddress: nativeTokenAddress, Symbol: "ETH", CoingeckoID: "ethereum", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDBerachain, TokenAddress: nativeTokenAddress, Symbol: "BERA", CoingeckoID: "berachain-bera", Decimals: 18},
|
||||
//{TokenChain: sdk.ChainIDWormchain, TokenAddress: nativeTokenAddress, Symbol: "WORM", CoingeckoID: "wormchain", Decimals: 18}, // Currently Wormchain doesn't charge gas fees to wormhole messages. This may change in the future: https://docs.wormhole.com/wormhole/explore-wormhole/gateway
|
||||
{TokenChain: sdk.ChainIDCosmoshub, TokenAddress: nativeTokenAddress, Symbol: "ATOM", CoingeckoID: "cosmos", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDEvmos, TokenAddress: nativeTokenAddress, Symbol: "EVMOS", CoingeckoID: "evmos", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDKujira, TokenAddress: nativeTokenAddress, Symbol: "KUJI", CoingeckoID: "kujira", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDNeutron, TokenAddress: nativeTokenAddress, Symbol: "NEUT", CoingeckoID: "neutron-3", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDCelestia, TokenAddress: nativeTokenAddress, Symbol: "TIA", CoingeckoID: "celestia", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDStargaze, TokenAddress: nativeTokenAddress, Symbol: "STARS", CoingeckoID: "stargaze", Decimals: 6},
|
||||
{TokenChain: sdk.ChainIDSeda, TokenAddress: nativeTokenAddress, Symbol: "SEDA", CoingeckoID: "seda-2", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDDymension, TokenAddress: nativeTokenAddress, Symbol: "DYM", CoingeckoID: "dymension", Decimals: 18},
|
||||
{TokenChain: sdk.ChainIDProvenance, TokenAddress: nativeTokenAddress, Symbol: "HASH", CoingeckoID: "provenance-blockchain", Decimals: 9},
|
||||
}
|
||||
}
|
||||
|
||||
func unknownTokenList() []TokenMetadata {
|
||||
return []TokenMetadata{
|
||||
{TokenChain: 23, TokenAddress: "00000000000000000000000007dd5beaffb65b8ff2e575d500bdf324a05295dc", Symbol: "arbi", CoingeckoID: "arbipad", Decimals: 18},
|
||||
|
|
|
@ -17,6 +17,7 @@ SQS_AWS_REGION=
|
|||
P2P_NETWORK=mainnet
|
||||
AWS_IAM_ROLE=
|
||||
METRICS_ENABLED=true
|
||||
NOTIONAL_CACHE_CHANNEL=WORMSCAN:NOTIONAL
|
||||
|
||||
ACALA_BASE_URL=https://eth-rpc-acala.aca-api.network
|
||||
ACALA_REQUESTS_PER_MINUTE=12
|
||||
|
|
|
@ -17,6 +17,7 @@ SQS_AWS_REGION=
|
|||
P2P_NETWORK=testnet
|
||||
AWS_IAM_ROLE=
|
||||
METRICS_ENABLED=true
|
||||
NOTIONAL_CACHE_CHANNEL=WORMSCAN:NOTIONAL
|
||||
|
||||
ACALA_BASE_URL=https://acala-dev.aca-dev.network/eth/http
|
||||
ACALA_REQUESTS_PER_MINUTE=12
|
||||
|
|
|
@ -17,6 +17,7 @@ SQS_AWS_REGION=
|
|||
P2P_NETWORK=mainnet
|
||||
AWS_IAM_ROLE=
|
||||
METRICS_ENABLED=true
|
||||
NOTIONAL_CACHE_CHANNEL=WORMSCAN:NOTIONAL
|
||||
|
||||
ACALA_BASE_URL=https://eth-rpc-acala.aca-api.network
|
||||
ACALA_REQUESTS_PER_MINUTE=12
|
||||
|
|
|
@ -17,6 +17,7 @@ SQS_AWS_REGION=
|
|||
P2P_NETWORK=testnet
|
||||
AWS_IAM_ROLE=
|
||||
METRICS_ENABLED=true
|
||||
NOTIONAL_CACHE_CHANNEL=WORMSCAN:NOTIONAL
|
||||
|
||||
ACALA_BASE_URL=https://acala-dev.aca-dev.network/eth/http
|
||||
ACALA_REQUESTS_PER_MINUTE=12
|
||||
|
|
|
@ -75,6 +75,18 @@ spec:
|
|||
value: "/opt/tx-tracker/rpc-provider.json"
|
||||
- name: CONSUMER_WORKERS_SIZE
|
||||
value: "1"
|
||||
- name: NOTIONAL_CACHE_CHANNEL
|
||||
value: {{ .NOTIONAL_CACHE_CHANNEL }}
|
||||
- name: NOTIONAL_CACHE_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: config
|
||||
key: redis-uri
|
||||
- name: NOTIONAL_CACHE_PREFIX
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: config
|
||||
key: redis-prefix
|
||||
image: {{ .IMAGE_NAME }}
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
|
|
@ -3,14 +3,15 @@ package chains
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/txtracker/internal/metrics"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
"go.uber.org/zap"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -36,6 +37,8 @@ type ethGetTransactionReceiptResponse struct {
|
|||
|
||||
type apiEvm struct {
|
||||
chainId sdk.ChainID
|
||||
notionalCache *notional.NotionalCache
|
||||
p2pNetwork string
|
||||
}
|
||||
|
||||
func (e *apiEvm) FetchEvmTx(
|
||||
|
@ -75,12 +78,23 @@ func (e *apiEvm) FetchEvmTx(
|
|||
zap.Error(err),
|
||||
zap.String("txHash", txHash),
|
||||
zap.String("chainId", e.chainId.String()))
|
||||
} else if fee == "" {
|
||||
} else if fee == nil {
|
||||
txDetail.FeeDetail = nil
|
||||
} else {
|
||||
txDetail.FeeDetail.Fee = fee
|
||||
txDetail.FeeDetail.Fee = fee.String()
|
||||
if e.p2pNetwork == domain.P2pMainNet {
|
||||
gasPrice, errGasPrice := GetGasTokenNotional(e.chainId, e.notionalCache)
|
||||
if errGasPrice != nil {
|
||||
logger.Error("Failed to get gas price",
|
||||
zap.Error(errGasPrice),
|
||||
zap.String("chainId", e.chainId.String()),
|
||||
zap.String("txHash", txHash))
|
||||
} else {
|
||||
txDetail.FeeDetail.GasTokenNotional = gasPrice.NotionalUsd.String()
|
||||
txDetail.FeeDetail.FeeUSD = gasPrice.NotionalUsd.Mul(*fee).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return txDetail, err
|
||||
|
@ -176,17 +190,17 @@ func (e *apiEvm) fetchEvmTxReceiptByTxHash(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func EvmCalculateFee(chainID sdk.ChainID, gasUsed string, effectiveGasPrice string) (string, error) {
|
||||
func EvmCalculateFee(chainID sdk.ChainID, gasUsed string, effectiveGasPrice string) (*decimal.Decimal, error) {
|
||||
//ignore if the blockchain is L2
|
||||
if chainID == sdk.ChainIDBase || chainID == sdk.ChainIDOptimism || chainID == sdk.ChainIDScroll {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get decimal gasUsed
|
||||
gs := new(big.Int)
|
||||
_, ok := gs.SetString(gasUsed, 0)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to convert gasUsed to big.Int")
|
||||
return nil, fmt.Errorf("failed to convert gasUsed to big.Int")
|
||||
}
|
||||
decimalGasUsed := decimal.NewFromBigInt(gs, 0)
|
||||
|
||||
|
@ -194,12 +208,12 @@ func EvmCalculateFee(chainID sdk.ChainID, gasUsed string, effectiveGasPrice stri
|
|||
gp := new(big.Int)
|
||||
_, ok = gp.SetString(effectiveGasPrice, 0)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to convert gasPrice to big.Int")
|
||||
return nil, fmt.Errorf("failed to convert gasPrice to big.Int")
|
||||
}
|
||||
decimalGasPrice := decimal.NewFromBigInt(gp, 0)
|
||||
|
||||
// calculate gasUsed * (gasPrice / 1e18)
|
||||
decimalFee := decimalGasUsed.Mul(decimalGasPrice)
|
||||
decimalFee = decimalFee.DivRound(decimal.NewFromInt(1e18), 18)
|
||||
return decimalFee.String(), nil
|
||||
return &decimalFee, nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
notional "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"time"
|
||||
|
||||
"github.com/mr-tron/base58"
|
||||
|
@ -59,6 +61,8 @@ type getTransactionConfig struct {
|
|||
|
||||
type apiSolana struct {
|
||||
timestamp *time.Time
|
||||
notionalCache *notional.NotionalCache
|
||||
p2pNetwork string
|
||||
}
|
||||
|
||||
func (a *apiSolana) FetchSolanaTx(
|
||||
|
@ -92,6 +96,16 @@ func (a *apiSolana) FetchSolanaTx(
|
|||
}
|
||||
}
|
||||
|
||||
if txDetail.FeeDetail != nil && txDetail.FeeDetail.Fee != "" && a.p2pNetwork == domain.P2pMainNet {
|
||||
gasPrice, errGasPrice := GetGasTokenNotional(sdk.ChainIDSolana, a.notionalCache)
|
||||
if errGasPrice != nil {
|
||||
logger.Error("Failed to get gas price", zap.Error(errGasPrice), zap.String("chainId", sdk.ChainIDSolana.String()), zap.String("txHash", txHash))
|
||||
} else {
|
||||
txDetail.FeeDetail.GasTokenNotional = gasPrice.NotionalUsd.String()
|
||||
txDetail.FeeDetail.FeeUSD = gasPrice.NotionalUsd.Mul(decimal.RequireFromString(txDetail.FeeDetail.Fee)).String()
|
||||
}
|
||||
}
|
||||
|
||||
return txDetail, err
|
||||
}
|
||||
|
||||
|
@ -196,15 +210,15 @@ func (a *apiSolana) fetchSolanaTx(
|
|||
"fee": fmt.Sprintf("%d", *response.Meta.Fee),
|
||||
},
|
||||
}
|
||||
feeDetail.Fee = SolanaCalculateFee(*response.Meta.Fee)
|
||||
feeDetail.Fee = SolanaCalculateFee(*response.Meta.Fee).String()
|
||||
txDetail.FeeDetail = feeDetail
|
||||
}
|
||||
|
||||
return &txDetail, nil
|
||||
}
|
||||
|
||||
func SolanaCalculateFee(fee uint64) string {
|
||||
func SolanaCalculateFee(fee uint64) decimal.Decimal {
|
||||
rawFee := decimal.NewFromUint64(fee)
|
||||
calculatedFee := rawFee.DivRound(decimal.NewFromInt(1e9), 9)
|
||||
return calculatedFee.String()
|
||||
return calculatedFee
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
notional "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
||||
|
@ -31,6 +32,8 @@ type TxDetail struct {
|
|||
type FeeDetail struct {
|
||||
Fee string `bson:"fee" json:"fee"`
|
||||
RawFee map[string]string `bson:"rawFee" json:"rawFee"`
|
||||
GasTokenNotional string `bson:"gasTokenNotional" json:"gasTokenNotional"`
|
||||
FeeUSD string `bson:"feeUSD" json:"feeUSD"`
|
||||
}
|
||||
|
||||
type AttributeTxDetail struct {
|
||||
|
@ -48,6 +51,7 @@ func FetchTx(
|
|||
p2pNetwork string,
|
||||
m metrics.Metrics,
|
||||
logger *zap.Logger,
|
||||
notionalCache *notional.NotionalCache,
|
||||
) (*TxDetail, error) {
|
||||
// Decide which RPC/API service to use based on chain ID
|
||||
var fetchFunc func(ctx context.Context, pool *pool.Pool, txHash string, metrics metrics.Metrics, logger *zap.Logger) (*TxDetail, error)
|
||||
|
@ -55,6 +59,8 @@ func FetchTx(
|
|||
case sdk.ChainIDSolana:
|
||||
apiSolana := &apiSolana{
|
||||
timestamp: timestamp,
|
||||
notionalCache: notionalCache,
|
||||
p2pNetwork: p2pNetwork,
|
||||
}
|
||||
fetchFunc = apiSolana.FetchSolanaTx
|
||||
case sdk.ChainIDAlgorand:
|
||||
|
@ -96,6 +102,8 @@ func FetchTx(
|
|||
sdk.ChainIDPolygonSepolia: // polygon amoy
|
||||
apiEvm := &apiEvm{
|
||||
chainId: chainId,
|
||||
notionalCache: notionalCache,
|
||||
p2pNetwork: p2pNetwork,
|
||||
}
|
||||
fetchFunc = apiEvm.FetchEvmTx
|
||||
case sdk.ChainIDWormchain:
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -145,3 +147,11 @@ func FormatTxHashByChain(chainId sdk.ChainID, txHash string) string {
|
|||
return txHash
|
||||
}
|
||||
}
|
||||
|
||||
func GetGasTokenNotional(chainID sdk.ChainID, notionalCache *notional.NotionalCache) (notional.PriceData, error) {
|
||||
nativeToken := domain.GetGasTokenMetadata(chainID)
|
||||
if nativeToken == nil {
|
||||
return notional.PriceData{}, fmt.Errorf("gas token not found for chain %s", chainID)
|
||||
}
|
||||
return notionalCache.Get(nativeToken.GetTokenID())
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package backfiller
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -103,6 +105,16 @@ func RunByVaas(backfillerConfig *VaasBackfiller) {
|
|||
// create a consumer repository.
|
||||
globalTrxRepository := consumer.NewRepository(logger, db.Database)
|
||||
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: cfg.NotionalCacheURL})
|
||||
notionalCache, errCache := notional.NewNotionalCache(ctx, redisClient, cfg.NotionalCachePrefix, cfg.NotionalCacheChannel, logger)
|
||||
if errCache != nil {
|
||||
logger.Fatal("Failed to create notional cache", zap.Error(errCache))
|
||||
}
|
||||
errCache = notionalCache.Init(ctx)
|
||||
if errCache != nil {
|
||||
logger.Fatal("Failed to initialize notional cache", zap.Error(errCache))
|
||||
}
|
||||
|
||||
query := repository.VaaQuery{
|
||||
StartTime: &startTime,
|
||||
EndTime: &endTime,
|
||||
|
@ -142,7 +154,7 @@ func RunByVaas(backfillerConfig *VaasBackfiller) {
|
|||
processedDocumentsSuccess: &quantityConsumedSuccess,
|
||||
processedDocumentsWithError: &quantityConsumedWithError,
|
||||
}
|
||||
go processVaa(ctx, &p)
|
||||
go processVaa(ctx, &p, notionalCache)
|
||||
}
|
||||
|
||||
logger.Info("Waiting for all workers to finish...")
|
||||
|
@ -195,7 +207,7 @@ func getVaas(ctx context.Context, logger *zap.Logger, pagination repository.Pagi
|
|||
}
|
||||
}
|
||||
|
||||
func processVaa(ctx context.Context, params *vaasBackfillerParams) {
|
||||
func processVaa(ctx context.Context, params *vaasBackfillerParams, cache *notional.NotionalCache) {
|
||||
// Main loop: fetch global txs and process them
|
||||
metrics := metrics.NewDummyMetrics()
|
||||
defer params.wg.Done()
|
||||
|
@ -226,7 +238,7 @@ func processVaa(ctx context.Context, params *vaasBackfillerParams) {
|
|||
Metrics: metrics,
|
||||
DisableDBUpsert: params.disableDBUpsert,
|
||||
}
|
||||
_, err := consumer.ProcessSourceTx(ctx, params.logger, params.rpcPool, params.wormchainRpcPool, params.repository, &p, params.p2pNetwork)
|
||||
_, err := consumer.ProcessSourceTx(ctx, params.logger, params.rpcPool, params.wormchainRpcPool, params.repository, &p, params.p2pNetwork, cache)
|
||||
if err != nil {
|
||||
if errors.Is(err, consumer.ErrAlreadyProcessed) {
|
||||
params.logger.Info("Source tx was already processed", zap.String("vaaId", v.ID))
|
||||
|
|
|
@ -3,6 +3,8 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -64,8 +66,18 @@ func Run() {
|
|||
repository := consumer.NewRepository(logger, db.Database)
|
||||
vaaRepository := vaa.NewRepository(db.Database, logger)
|
||||
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: cfg.NotionalCacheURL})
|
||||
notionalCache, errCache := notional.NewNotionalCache(rootCtx, redisClient, cfg.NotionalCachePrefix, cfg.NotionalCacheChannel, logger)
|
||||
if errCache != nil {
|
||||
logger.Fatal("Failed to create notional cache", zap.Error(errCache))
|
||||
}
|
||||
errCache = notionalCache.Init(rootCtx)
|
||||
if errCache != nil {
|
||||
logger.Fatal("Failed to initialize notional cache", zap.Error(errCache))
|
||||
}
|
||||
|
||||
// create controller
|
||||
vaaController := vaa.NewController(rpcPool, wormchainRpcPool, vaaRepository, repository, cfg.P2pNetwork, logger)
|
||||
vaaController := vaa.NewController(rpcPool, wormchainRpcPool, vaaRepository, repository, cfg.P2pNetwork, logger, notionalCache)
|
||||
|
||||
// start serving /health and /ready endpoints
|
||||
healthChecks, err := makeHealthChecks(rootCtx, cfg, db.Database)
|
||||
|
@ -77,12 +89,12 @@ func Run() {
|
|||
|
||||
// create and start a pipeline consumer.
|
||||
vaaConsumeFunc := newVAAConsumeFunc(rootCtx, cfg, metrics, logger)
|
||||
vaaConsumer := consumer.New(vaaConsumeFunc, rpcPool, wormchainRpcPool, rootCtx, logger, repository, metrics, cfg.P2pNetwork, cfg.ConsumerWorkersSize)
|
||||
vaaConsumer := consumer.New(vaaConsumeFunc, rpcPool, wormchainRpcPool, logger, repository, metrics, cfg.P2pNetwork, cfg.ConsumerWorkersSize, notionalCache)
|
||||
vaaConsumer.Start(rootCtx)
|
||||
|
||||
// create and start a notification consumer.
|
||||
notificationConsumeFunc := newNotificationConsumeFunc(rootCtx, cfg, metrics, logger)
|
||||
notificationConsumer := consumer.New(notificationConsumeFunc, rpcPool, wormchainRpcPool, rootCtx, logger, repository, metrics, cfg.P2pNetwork, cfg.ConsumerWorkersSize)
|
||||
notificationConsumer := consumer.New(notificationConsumeFunc, rpcPool, wormchainRpcPool, logger, repository, metrics, cfg.P2pNetwork, cfg.ConsumerWorkersSize, notionalCache)
|
||||
notificationConsumer.Start(rootCtx)
|
||||
|
||||
logger.Info("Started wormhole-explorer-tx-tracker")
|
||||
|
|
|
@ -24,6 +24,9 @@ type ServiceSettings struct {
|
|||
P2pNetwork string `split_words:"true" required:"true"`
|
||||
RpcProviderPath string `split_words:"true" required:"false"`
|
||||
ConsumerWorkersSize int `split_words:"true" default:"10"`
|
||||
NotionalCacheURL string `split_words:"true" required:"true"`
|
||||
NotionalCachePrefix string `split_words:"true" required:"true"`
|
||||
NotionalCacheChannel string `split_words:"true" required:"true"`
|
||||
AwsSettings
|
||||
MongodbSettings
|
||||
*RpcProviderSettings `required:"false"`
|
||||
|
@ -35,6 +38,9 @@ type ServiceSettings struct {
|
|||
type RpcProviderSettingsJson struct {
|
||||
RpcProviders []ChainRpcProviderSettings `json:"rpcProviders"`
|
||||
WormchainRpcProviders []ChainRpcProviderSettings `json:"wormchainRpcProviders"`
|
||||
NotionalCacheURL string `json:"notional_cache_url"`
|
||||
NotionalCachePrefix string `json:"notional_cache_prefix"`
|
||||
NotionalCacheChannel string `json:"notional_cache_channel"`
|
||||
}
|
||||
|
||||
type ChainRpcProviderSettings struct {
|
||||
|
|
|
@ -3,6 +3,7 @@ package consumer
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
||||
|
@ -24,19 +25,19 @@ type Consumer struct {
|
|||
metrics metrics.Metrics
|
||||
p2pNetwork string
|
||||
workersSize int
|
||||
notionalCache *notional.NotionalCache
|
||||
}
|
||||
|
||||
// New creates a new vaa consumer.
|
||||
func New(
|
||||
consumeFunc queue.ConsumeFunc,
|
||||
func New(consumeFunc queue.ConsumeFunc,
|
||||
rpcPool map[vaa.ChainID]*pool.Pool,
|
||||
wormchainRpcPool map[vaa.ChainID]*pool.Pool,
|
||||
ctx context.Context,
|
||||
logger *zap.Logger,
|
||||
repository *Repository,
|
||||
metrics metrics.Metrics,
|
||||
p2pNetwork string,
|
||||
workersSize int,
|
||||
notionalCache *notional.NotionalCache,
|
||||
) *Consumer {
|
||||
|
||||
c := Consumer{
|
||||
|
@ -48,6 +49,7 @@ func New(
|
|||
metrics: metrics,
|
||||
p2pNetwork: p2pNetwork,
|
||||
workersSize: workersSize,
|
||||
notionalCache: notionalCache,
|
||||
}
|
||||
|
||||
return &c
|
||||
|
@ -118,7 +120,7 @@ func (c *Consumer) processSourceTx(ctx context.Context, msg queue.ConsumerMessag
|
|||
Source: event.Source,
|
||||
SentTimestamp: msg.SentTimestamp(),
|
||||
}
|
||||
_, err := ProcessSourceTx(ctx, c.logger, c.rpcpool, c.wormchainRpcPool, c.repository, &p, c.p2pNetwork)
|
||||
_, err := ProcessSourceTx(ctx, c.logger, c.rpcpool, c.wormchainRpcPool, c.repository, &p, c.p2pNetwork, c.notionalCache)
|
||||
|
||||
// add vaa processing duration metrics
|
||||
c.metrics.AddVaaProcessedDuration(uint16(event.ChainID), time.Since(start).Seconds())
|
||||
|
@ -204,8 +206,9 @@ func (c *Consumer) processTargetTx(ctx context.Context, msg queue.ConsumerMessag
|
|||
EvmFee: evmFee,
|
||||
SolanaFee: solanaFee,
|
||||
Metrics: c.metrics,
|
||||
P2pNetwork: c.p2pNetwork,
|
||||
}
|
||||
err := ProcessTargetTx(ctx, c.logger, c.repository, &p)
|
||||
err := ProcessTargetTx(ctx, c.logger, c.repository, &p, c.notionalCache)
|
||||
|
||||
elapsedLog := zap.Uint64("elapsedTime", uint64(time.Since(start).Milliseconds()))
|
||||
if err != nil {
|
||||
|
|
|
@ -34,6 +34,8 @@ type DestinationTx struct {
|
|||
type FeeDetail struct {
|
||||
Fee string `bson:"fee"`
|
||||
RawFee map[string]string `bson:"rawFee"`
|
||||
GasTokenNotional string `bson:"gasTokenNotional" json:"gasTokenNotional"`
|
||||
FeeUSD string `bson:"feeUSD" json:"feeUSD"`
|
||||
}
|
||||
|
||||
// TargetTxUpdate represents a transaction document.
|
||||
|
|
|
@ -3,6 +3,7 @@ package consumer
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
notionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
|
@ -38,6 +39,7 @@ type ProcessSourceTxParams struct {
|
|||
Metrics metrics.Metrics
|
||||
SentTimestamp *time.Time
|
||||
DisableDBUpsert bool
|
||||
P2pNetwork string
|
||||
}
|
||||
|
||||
func ProcessSourceTx(
|
||||
|
@ -48,6 +50,7 @@ func ProcessSourceTx(
|
|||
repository *Repository,
|
||||
params *ProcessSourceTxParams,
|
||||
p2pNetwork string,
|
||||
notionalCache *notionalCache.NotionalCache,
|
||||
) (*chains.TxDetail, error) {
|
||||
|
||||
if !params.Overwrite {
|
||||
|
@ -114,7 +117,7 @@ func ProcessSourceTx(
|
|||
}
|
||||
|
||||
// Get transaction details from the emitter blockchain
|
||||
txDetail, err = chains.FetchTx(ctx, rpcPool, wormchainRpcPool, params.ChainId, params.TxHash, params.Timestamp, p2pNetwork, params.Metrics, logger)
|
||||
txDetail, err = chains.FetchTx(ctx, rpcPool, wormchainRpcPool, params.ChainId, params.TxHash, params.Timestamp, p2pNetwork, params.Metrics, logger, notionalCache)
|
||||
if err != nil {
|
||||
errHandleFetchTx := handleFetchTxError(ctx, logger, repository, params, err)
|
||||
if errHandleFetchTx == nil {
|
||||
|
|
|
@ -3,6 +3,8 @@ package consumer
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -36,6 +38,7 @@ type ProcessTargetTxParams struct {
|
|||
EvmFee *EvmFee
|
||||
SolanaFee *SolanaFee
|
||||
Metrics metrics.Metrics
|
||||
P2pNetwork string
|
||||
}
|
||||
|
||||
type EvmFee struct {
|
||||
|
@ -52,9 +55,10 @@ func ProcessTargetTx(
|
|||
logger *zap.Logger,
|
||||
repository *Repository,
|
||||
params *ProcessTargetTxParams,
|
||||
notionalCache *notional.NotionalCache,
|
||||
) error {
|
||||
|
||||
feeDetail := calculateFeeDetail(params, logger)
|
||||
feeDetail := calculateFeeDetail(params, logger, notionalCache)
|
||||
|
||||
txHash := domain.NormalizeTxHashByChainId(params.ChainID, params.TxHash)
|
||||
now := time.Now()
|
||||
|
@ -119,8 +123,10 @@ func checkTxShouldBeUpdated(ctx context.Context, tx *TargetTxUpdate, repository
|
|||
}
|
||||
}
|
||||
|
||||
func calculateFeeDetail(params *ProcessTargetTxParams, logger *zap.Logger) *FeeDetail {
|
||||
func calculateFeeDetail(params *ProcessTargetTxParams, logger *zap.Logger, notionalCache *notional.NotionalCache) *FeeDetail {
|
||||
|
||||
// calculate tx fee for evm redeemed tx.
|
||||
var feeDetail *FeeDetail
|
||||
if params.EvmFee != nil {
|
||||
fee, err := chains.EvmCalculateFee(params.ChainID, params.EvmFee.GasUsed, params.EvmFee.EffectiveGasPrice)
|
||||
if err != nil {
|
||||
|
@ -133,27 +139,40 @@ func calculateFeeDetail(params *ProcessTargetTxParams, logger *zap.Logger) *FeeD
|
|||
)
|
||||
return nil
|
||||
}
|
||||
if fee == "" {
|
||||
return nil
|
||||
}
|
||||
return &FeeDetail{
|
||||
if fee != nil {
|
||||
feeDetail = &FeeDetail{
|
||||
RawFee: map[string]string{
|
||||
"gasUsed": params.EvmFee.GasUsed,
|
||||
"effectiveGasPrice": params.EvmFee.EffectiveGasPrice,
|
||||
},
|
||||
Fee: fee,
|
||||
Fee: fee.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
// calculate tx fee for solana redeemed tx.
|
||||
if params.SolanaFee != nil {
|
||||
fee := chains.SolanaCalculateFee(params.SolanaFee.Fee)
|
||||
return &FeeDetail{
|
||||
feeDetail = &FeeDetail{
|
||||
RawFee: map[string]string{
|
||||
"fee": strconv.FormatUint(params.SolanaFee.Fee, 10),
|
||||
},
|
||||
Fee: fee,
|
||||
Fee: fee.String(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if feeDetail != nil && params.P2pNetwork == domain.P2pMainNet {
|
||||
gasTokenPrice, errGasPrice := chains.GetGasTokenNotional(params.ChainID, notionalCache)
|
||||
if errGasPrice != nil {
|
||||
logger.Error("Failed to get gas price",
|
||||
zap.Error(errGasPrice),
|
||||
zap.String("chainId", params.ChainID.String()),
|
||||
zap.String("txHash", params.TxHash),
|
||||
)
|
||||
return feeDetail
|
||||
}
|
||||
feeDetail.GasTokenNotional = gasTokenPrice.NotionalUsd.String()
|
||||
feeDetail.FeeUSD = gasTokenPrice.NotionalUsd.Mul(decimal.RequireFromString(feeDetail.Fee)).String()
|
||||
}
|
||||
|
||||
return feeDetail
|
||||
}
|
||||
|
|
|
@ -2,10 +2,8 @@ package vaa
|
|||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/utils"
|
||||
|
@ -13,6 +11,8 @@ import (
|
|||
"github.com/wormhole-foundation/wormhole-explorer/txtracker/internal/metrics"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Controller definition.
|
||||
|
@ -24,10 +24,11 @@ type Controller struct {
|
|||
repository *consumer.Repository
|
||||
metrics metrics.Metrics
|
||||
p2pNetwork string
|
||||
notionalCache *notional.NotionalCache
|
||||
}
|
||||
|
||||
// NewController creates a Controller instance.
|
||||
func NewController(rpcPool map[sdk.ChainID]*pool.Pool, wormchainRpcPool map[sdk.ChainID]*pool.Pool, vaaRepository *Repository, repository *consumer.Repository, p2pNetwork string, logger *zap.Logger) *Controller {
|
||||
func NewController(rpcPool map[sdk.ChainID]*pool.Pool, wormchainRpcPool map[sdk.ChainID]*pool.Pool, vaaRepository *Repository, repository *consumer.Repository, p2pNetwork string, logger *zap.Logger, notionalCache *notional.NotionalCache) *Controller {
|
||||
return &Controller{
|
||||
metrics: metrics.NewDummyMetrics(),
|
||||
rpcPool: rpcPool,
|
||||
|
@ -35,7 +36,9 @@ func NewController(rpcPool map[sdk.ChainID]*pool.Pool, wormchainRpcPool map[sdk.
|
|||
vaaRepository: vaaRepository,
|
||||
repository: repository,
|
||||
p2pNetwork: p2pNetwork,
|
||||
logger: logger}
|
||||
logger: logger,
|
||||
notionalCache: notionalCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) Process(ctx *fiber.Ctx) error {
|
||||
|
@ -70,9 +73,10 @@ func (c *Controller) Process(ctx *fiber.Ctx) error {
|
|||
IsVaaSigned: true,
|
||||
Metrics: c.metrics,
|
||||
Overwrite: true,
|
||||
P2pNetwork: c.p2pNetwork,
|
||||
}
|
||||
|
||||
result, err := consumer.ProcessSourceTx(ctx.Context(), c.logger, c.rpcPool, c.wormchainRpcPool, c.repository, p, c.p2pNetwork)
|
||||
result, err := consumer.ProcessSourceTx(ctx.Context(), c.logger, c.rpcPool, c.wormchainRpcPool, c.repository, p, c.p2pNetwork, c.notionalCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -143,7 +147,7 @@ func (c *Controller) CreateTxHash(ctx *fiber.Ctx) error {
|
|||
DisableDBUpsert: true,
|
||||
}
|
||||
|
||||
result, err := consumer.ProcessSourceTx(ctx.Context(), c.logger, c.rpcPool, c.wormchainRpcPool, c.repository, p, c.p2pNetwork)
|
||||
result, err := consumer.ProcessSourceTx(ctx.Context(), c.logger, c.rpcPool, c.wormchainRpcPool, c.repository, p, c.p2pNetwork, c.notionalCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue