Add support for cosmos chains in `tx-tracker` (#426)

### Description

Tracking issue: https://github.com/wormhole-foundation/wormhole-explorer/issues/418

This pull request modifies the `tx-tracker` service to support `ChainIDTerra2` and `ChainIDXpla`. In particular, this will make sender addresses from these blockchains available for the Wormhole Scan UI.

Support for `ChainIDTerra` was left out due to lack of working public RPC nodes (`https://lcd.terra.dev` doesn't seem to be functioning correctly at the moment).
This commit is contained in:
agodnic 2023-06-21 15:47:12 -03:00 committed by GitHub
parent 2c476dd696
commit 4b4b6f61ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 385 additions and 16 deletions

View File

@ -6,5 +6,48 @@ RESOURCES_LIMITS_MEMORY=256Mi
RESOURCES_LIMITS_CPU=500m
RESOURCES_REQUESTS_MEMORY=128Mi
RESOURCES_REQUESTS_CPU=250m
SOLANA_BASE_URL=https://api.mainnet-beta.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=2
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=2
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=2
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=2
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=2
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=2
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=2
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=2
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=2
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=2
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=2
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=2
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=2
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=2
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=2

View File

@ -7,9 +7,51 @@ RESOURCES_LIMITS_CPU=500m
RESOURCES_REQUESTS_MEMORY=64Mi
RESOURCES_REQUESTS_CPU=250m
SOLANA_BASE_URL=https://api.mainnet-beta.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
STRATEGY_NAME=time_range
STRATEGY_TIMESTAMP_AFTER=2023-01-01T00:00:00.000Z
STRATEGY_TIMESTAMP_BEFORE=2023-04-01T00:00:00.000Z
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=1
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=1
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=1
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=1
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=1
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=1
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=1
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=1
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=1
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=1
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=1
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=1
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=1
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=1
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=1

View File

@ -6,5 +6,48 @@ RESOURCES_LIMITS_MEMORY=128Mi
RESOURCES_LIMITS_CPU=200m
RESOURCES_REQUESTS_MEMORY=64Mi
RESOURCES_REQUESTS_CPU=100m
SOLANA_BASE_URL=https://api.mainnet-beta.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=1
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=1
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=1
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=1
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=1
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=1
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=1
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=1
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=1
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=1
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=1
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=1
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=1
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=1
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=1

View File

@ -83,6 +83,14 @@ spec:
value: {{ .SUI_BASE_URL }}
- name: SUI_REQUESTS_PER_MINUTE
value: "{{ .SUI_REQUESTS_PER_MINUTE }}"
- name: TERRA2_BASE_URL
value: {{ .TERRA2_BASE_URL }}
- name: TERRA2_REQUESTS_PER_MINUTE
value: "{{ .TERRA2_REQUESTS_PER_MINUTE }}"
- name: XPLA_BASE_URL
value: {{ .XPLA_BASE_URL }}
- name: XPLA_REQUESTS_PER_MINUTE
value: "{{ .XPLA_REQUESTS_PER_MINUTE }}"
- name: NUM_WORKERS
value: "100"
- name: BULK_SIZE

View File

@ -9,6 +9,49 @@ RESOURCES_REQUESTS_MEMORY=128Mi
RESOURCES_REQUESTS_CPU=250m
SQS_URL=
SQS_AWS_REGION=
SOLANA_BASE_URL=https://api.mainnet-beta.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
AWS_IAM_ROLE=
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=8
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=8
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=4
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=8
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=8
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=8
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=8
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=8
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=8
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=8
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=8
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=4
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=4
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=4
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=4

View File

@ -9,7 +9,49 @@ RESOURCES_REQUESTS_MEMORY=15Mi
RESOURCES_REQUESTS_CPU=40m
SQS_URL=
SQS_AWS_REGION=
AWS_IAM_ROLE=
SOLANA_BASE_URL=https://api.mainnet-beta.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
AWS_IAM_ROLE=
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=2
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=2
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=2
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=2
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=2
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=2
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=2
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=2
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=2
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=2
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=2
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=2
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=2
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=2
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=2

View File

@ -9,6 +9,49 @@ RESOURCES_REQUESTS_MEMORY=15Mi
RESOURCES_REQUESTS_CPU=10m
SQS_URL=
SQS_AWS_REGION=
SOLANA_BASE_URL=https://api.devnet.solana.com
SOLANA_REQUESTS_PER_MINUTE=6
AWS_IAM_ROLE=
AWS_IAM_ROLE=
APTOS_BASE_URL=
APTOS_REQUESTS_PER_MINUTE=2
ARBITRUM_BASE_URL=
ARBITRUM_REQUESTS_PER_MINUTE=2
AVALANCHE_BASE_URL=
AVALANCHE_REQUESTS_PER_MINUTE=2
BSC_BASE_URL=
BSC_REQUESTS_PER_MINUTE=2
CELO_BASE_URL=
CELO_REQUESTS_PER_MINUTE=2
ETHEREUM_BASE_URL=
ETHEREUM_REQUESTS_PER_MINUTE=2
FANTOM_BASE_URL=
FANTOM_REQUESTS_PER_MINUTE=2
KLAYTN_BASE_URL=
KLAYTN_REQUESTS_PER_MINUTE=2
MOONBEAM_BASE_URL=
MOONBEAM_REQUESTS_PER_MINUTE=2
OPTIMISM_BASE_URL=
OPTIMISM_REQUESTS_PER_MINUTE=2
POLYGON_BASE_URL=
POLYGON_REQUESTS_PER_MINUTE=2
SOLANA_BASE_URL=
SOLANA_REQUESTS_PER_MINUTE=2
SUI_BASE_URL=
SUI_REQUESTS_PER_MINUTE=2
TERRA2_BASE_URL=
TERRA2_REQUESTS_PER_MINUTE=2
XPLA_BASE_URL=
XPLA_REQUESTS_PER_MINUTE=2

View File

@ -109,6 +109,14 @@ spec:
value: {{ .SUI_BASE_URL }}
- name: SUI_REQUESTS_PER_MINUTE
value: "{{ .SUI_REQUESTS_PER_MINUTE }}"
- name: TERRA2_BASE_URL
value: {{ .TERRA2_BASE_URL }}
- name: TERRA2_REQUESTS_PER_MINUTE
value: "{{ .TERRA2_REQUESTS_PER_MINUTE }}"
- name: XPLA_BASE_URL
value: {{ .XPLA_BASE_URL }}
- name: XPLA_REQUESTS_PER_MINUTE
value: "{{ .XPLA_REQUESTS_PER_MINUTE }}"
resources:
limits:
memory: {{ .RESOURCES_LIMITS_MEMORY }}

View File

@ -0,0 +1,76 @@
package chains
import (
"context"
"encoding/json"
"fmt"
"time"
)
const (
cosmosMsgExecuteContract = "/cosmwasm.wasm.v1.MsgExecuteContract"
)
// cosmosTxsResponse models the response body from `GET /cosmos/tx/v1beta1/txs/{hash}`
type cosmosTxsResponse struct {
TxResponse struct {
Tx struct {
Body struct {
Messages []struct {
Type_ string `json:"@type"`
Sender string `json:"sender"`
} `json:"messages"`
} `json:"body"`
} `json:"tx"`
Timestamp string `json:"timestamp"`
TxHash string `json:"txhash"`
} `json:"tx_response"`
}
func fetchCosmosTx(
ctx context.Context,
baseUri string,
txHash string,
) (*TxDetail, error) {
// Query the Cosmos transaction endpoint
uri := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", baseUri, txHash)
body, err := httpGet(ctx, uri)
if err != nil {
return nil, fmt.Errorf("failed to query cosmos tx endpoint: %w", err)
}
// Deserialize response body
var response cosmosTxsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("failed to deserialize cosmos tx response: %w", err)
}
// Find the sender address
var sender string
for i := range response.TxResponse.Tx.Body.Messages {
msg := &response.TxResponse.Tx.Body.Messages[i]
if msg.Type_ == cosmosMsgExecuteContract {
sender = msg.Sender
break
}
}
if sender == "" {
return nil, fmt.Errorf("failed to find sender address in cosmos tx response")
}
// Parse the timestamp
timestamp, err := time.Parse("2006-01-02T15:04:05Z", response.TxResponse.Timestamp)
if err != nil {
return nil, fmt.Errorf("failed to parse tx timestamp from cosmos tx response: %w", err)
}
// Build the result object and return
TxDetail := &TxDetail{
From: sender,
Timestamp: timestamp,
NativeTxHash: response.TxResponse.TxHash,
}
return TxDetail, nil
}

View File

@ -45,6 +45,8 @@ var tickers = struct {
polygon *time.Ticker
solana *time.Ticker
sui *time.Ticker
terra2 *time.Ticker
xpla *time.Ticker
}{}
func Initialize(cfg *config.RpcProviderSettings) {
@ -58,8 +60,10 @@ func Initialize(cfg *config.RpcProviderSettings) {
return time.Duration(roundedUp)
}
// this adapter sends 1 request per txHash
// these adapters send 1 request per txHash
tickers.sui = time.NewTicker(f(cfg.SuiRequestsPerMinute))
tickers.terra2 = time.NewTicker(f(cfg.Terra2RequestsPerMinute))
tickers.xpla = time.NewTicker(f(cfg.XplaRequestsPerMinute))
// these adapters send 2 requests per txHash
tickers.aptos = time.NewTicker(f(cfg.AptosRequestsPerMinute / 2))
@ -147,6 +151,16 @@ func FetchTx(
case vaa.ChainIDSui:
fetchFunc = fetchSuiTx
rateLimiter = *tickers.sui
case vaa.ChainIDTerra2:
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
return fetchCosmosTx(ctx, cfg.Terra2BaseUrl, txHash)
}
rateLimiter = *tickers.terra2
case vaa.ChainIDXpla:
fetchFunc = func(ctx context.Context, cfg *config.RpcProviderSettings, txHash string) (*TxDetail, error) {
return fetchCosmosTx(ctx, cfg.XplaBaseUrl, txHash)
}
rateLimiter = *tickers.xpla
default:
return nil, ErrChainNotSupported
}
@ -203,6 +217,9 @@ func httpGet(ctx context.Context, url string) ([]byte, error) {
return nil, fmt.Errorf("failed to query url: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected HTTP status code: %d", response.StatusCode)
}
// Read the response body and return
body, err := ioutil.ReadAll(response.Body)

View File

@ -85,6 +85,10 @@ type RpcProviderSettings struct {
SolanaRequestsPerMinute uint16 `split_words:"true" required:"true"`
SuiBaseUrl string `split_words:"true" required:"true"`
SuiRequestsPerMinute uint16 `split_words:"true" required:"true"`
Terra2BaseUrl string `split_words:"true" required:"true"`
Terra2RequestsPerMinute uint16 `split_words:"true" required:"true"`
XplaBaseUrl string `split_words:"true" required:"true"`
XplaRequestsPerMinute uint16 `split_words:"true" required:"true"`
}
func LoadFromEnv[T any]() (*T, error) {