Add support for Aptos in the `tx-tracker` service (#421)
### Description This pull request adds support for Aptos in the `tx-tracker` service. This will lead to improvements for the wormhole Scan UI: 1. The transaction hash that was being previously displayed for Aptos VAAs was incorrect. This pull request fixes the issue. 2. The sender addresses for Aptos VAAs will now become available for for the UI.
This commit is contained in:
parent
ba3d1d9e61
commit
3f823c51ac
|
@ -399,14 +399,17 @@ func (c *Controller) ListTransactions(ctx *fiber.Ctx) error {
|
||||||
UsdAmount: queryResult.Transactions[i].UsdAmount,
|
UsdAmount: queryResult.Transactions[i].UsdAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Solana VAAs, the txHash that we get from the gossip network is not the real transacion hash,
|
// Set the transaction hash
|
||||||
// so we have to overwrite it with the real txHash.
|
isSolanaOrAptos := queryResult.Transactions[i].EmitterChain == sdk.ChainIDSolana ||
|
||||||
if queryResult.Transactions[i].EmitterChain == sdk.ChainIDSolana &&
|
queryResult.Transactions[i].EmitterChain == sdk.ChainIDAptos
|
||||||
len(queryResult.Transactions[i].GlobalTransations) == 1 &&
|
if isSolanaOrAptos {
|
||||||
|
// For Solana and Aptos VAAs, the txHash that we get from the gossip network is
|
||||||
|
// not the real transacion hash. We have to overwrite it with the real one.
|
||||||
|
if len(queryResult.Transactions[i].GlobalTransations) == 1 &&
|
||||||
queryResult.Transactions[i].GlobalTransations[0].OriginTx != nil {
|
queryResult.Transactions[i].GlobalTransations[0].OriginTx != nil {
|
||||||
|
|
||||||
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash
|
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tx.TxHash = queryResult.Transactions[i].TxHash
|
tx.TxHash = queryResult.Transactions[i].TxHash
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,10 @@ spec:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: config
|
name: config
|
||||||
key: mongo-database
|
key: mongo-database
|
||||||
|
- name: APTOS_BASE_URL
|
||||||
|
value: {{ .APTOS_BASE_URL }}
|
||||||
|
- name: APTOS_REQUESTS_PER_MINUTE
|
||||||
|
value: "{{ .APTOS_REQUESTS_PER_MINUTE }}"
|
||||||
- name: ARBITRUM_BASE_URL
|
- name: ARBITRUM_BASE_URL
|
||||||
value: {{ .ARBITRUM_BASE_URL }}
|
value: {{ .ARBITRUM_BASE_URL }}
|
||||||
- name: ARBITRUM_REQUESTS_PER_MINUTE
|
- name: ARBITRUM_REQUESTS_PER_MINUTE
|
||||||
|
|
|
@ -57,6 +57,10 @@ spec:
|
||||||
value: {{ .SQS_URL }}
|
value: {{ .SQS_URL }}
|
||||||
- name: AWS_REGION
|
- name: AWS_REGION
|
||||||
value: {{ .SQS_AWS_REGION }}
|
value: {{ .SQS_AWS_REGION }}
|
||||||
|
- name: APTOS_BASE_URL
|
||||||
|
value: {{ .APTOS_BASE_URL }}
|
||||||
|
- name: APTOS_REQUESTS_PER_MINUTE
|
||||||
|
value: "{{ .APTOS_REQUESTS_PER_MINUTE }}"
|
||||||
- name: ARBITRUM_BASE_URL
|
- name: ARBITRUM_BASE_URL
|
||||||
value: {{ .ARBITRUM_BASE_URL }}
|
value: {{ .ARBITRUM_BASE_URL }}
|
||||||
- name: ARBITRUM_REQUESTS_PER_MINUTE
|
- name: ARBITRUM_REQUESTS_PER_MINUTE
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package chains
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/txtracker/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
aptosCoreContractAddress = "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625"
|
||||||
|
)
|
||||||
|
|
||||||
|
type aptosEvent struct {
|
||||||
|
Version uint64 `json:"version,string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type aptosTx struct {
|
||||||
|
Timestamp uint64 `json:"timestamp,string"`
|
||||||
|
Sender string `json:"sender"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAptosTx(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg *config.RpcProviderSettings,
|
||||||
|
txHash string,
|
||||||
|
) (*TxDetail, error) {
|
||||||
|
|
||||||
|
// Parse the Aptos event creation number
|
||||||
|
creationNumber, err := strconv.ParseUint(txHash, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse event creation number from Aptos tx hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the event from the Aptos node API.
|
||||||
|
var events []aptosEvent
|
||||||
|
{
|
||||||
|
// Build the URI for the events endpoint
|
||||||
|
uri := fmt.Sprintf("%s/accounts/%s/events/%s::state::WormholeMessageHandle/event?start=%d&limit=1",
|
||||||
|
cfg.AptosBaseUrl,
|
||||||
|
aptosCoreContractAddress,
|
||||||
|
aptosCoreContractAddress,
|
||||||
|
creationNumber,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query the events endpoint
|
||||||
|
body, err := httpGet(ctx, uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query events endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the response
|
||||||
|
err = json.Unmarshal(body, &events)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response body from events endpoint: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(events) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected exactly one event, but got %d", len(events))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the transacton
|
||||||
|
var tx aptosTx
|
||||||
|
{
|
||||||
|
// Build the URI for the events endpoint
|
||||||
|
uri := fmt.Sprintf("%s/transactions/by_version/%d", cfg.AptosBaseUrl, events[0].Version)
|
||||||
|
|
||||||
|
// Query the events endpoint
|
||||||
|
body, err := httpGet(ctx, uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query transactions endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the response
|
||||||
|
err = json.Unmarshal(body, &tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response body from transactions endpoint: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the result struct and return
|
||||||
|
TxDetail := TxDetail{
|
||||||
|
NativeTxHash: tx.Hash,
|
||||||
|
From: tx.Sender,
|
||||||
|
Timestamp: time.UnixMicro(int64(tx.Timestamp)),
|
||||||
|
}
|
||||||
|
return &TxDetail, nil
|
||||||
|
}
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -39,6 +41,7 @@ var tickers = struct {
|
||||||
optimism *time.Ticker
|
optimism *time.Ticker
|
||||||
polygon *time.Ticker
|
polygon *time.Ticker
|
||||||
solana *time.Ticker
|
solana *time.Ticker
|
||||||
|
aptos *time.Ticker
|
||||||
sui *time.Ticker
|
sui *time.Ticker
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
@ -57,6 +60,7 @@ func Initialize(cfg *config.RpcProviderSettings) {
|
||||||
tickers.sui = time.NewTicker(f(cfg.SuiRequestsPerMinute))
|
tickers.sui = time.NewTicker(f(cfg.SuiRequestsPerMinute))
|
||||||
|
|
||||||
// these adapters send 2 requests per txHash
|
// these adapters send 2 requests per txHash
|
||||||
|
tickers.aptos = time.NewTicker(f(cfg.AptosRequestsPerMinute / 2))
|
||||||
tickers.arbitrum = time.NewTicker(f(cfg.ArbitrumRequestsPerMinute / 2))
|
tickers.arbitrum = time.NewTicker(f(cfg.ArbitrumRequestsPerMinute / 2))
|
||||||
tickers.avalanche = time.NewTicker(f(cfg.AvalancheRequestsPerMinute / 2))
|
tickers.avalanche = time.NewTicker(f(cfg.AvalancheRequestsPerMinute / 2))
|
||||||
tickers.bsc = time.NewTicker(f(cfg.BscRequestsPerMinute / 2))
|
tickers.bsc = time.NewTicker(f(cfg.BscRequestsPerMinute / 2))
|
||||||
|
@ -123,6 +127,9 @@ func FetchTx(
|
||||||
return fetchEthTx(ctx, txHash, cfg.AvalancheBaseUrl)
|
return fetchEthTx(ctx, txHash, cfg.AvalancheBaseUrl)
|
||||||
}
|
}
|
||||||
rateLimiter = *tickers.avalanche
|
rateLimiter = *tickers.avalanche
|
||||||
|
case vaa.ChainIDAptos:
|
||||||
|
fetchFunc = fetchAptosTx
|
||||||
|
rateLimiter = *tickers.aptos
|
||||||
case vaa.ChainIDSui:
|
case vaa.ChainIDSui:
|
||||||
fetchFunc = fetchSuiTx
|
fetchFunc = fetchSuiTx
|
||||||
rateLimiter = *tickers.sui
|
rateLimiter = *tickers.sui
|
||||||
|
@ -165,3 +172,28 @@ func timestampFromHex(s string) (time.Time, error) {
|
||||||
timestamp := time.Unix(epoch, 0).UTC()
|
timestamp := time.Unix(epoch, 0).UTC()
|
||||||
return timestamp, nil
|
return timestamp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// httpGet is a helper function that performs an HTTP request.
|
||||||
|
func httpGet(ctx context.Context, url string) ([]byte, error) {
|
||||||
|
|
||||||
|
// Build the HTTP request
|
||||||
|
request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send it
|
||||||
|
var client http.Client
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query url: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Read the response body and return
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,5 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// print tx details
|
// print tx details
|
||||||
log.Printf("tx detail: sender=%s nativeTxHash=%s timestamp=%s",
|
log.Printf("tx detail: %+v", txDetail)
|
||||||
txDetail.From, txDetail.NativeTxHash, txDetail.Timestamp)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,8 @@ type MongodbSettings struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RpcProviderSettings struct {
|
type RpcProviderSettings struct {
|
||||||
|
AptosBaseUrl string `split_words:"true" required:"true"`
|
||||||
|
AptosRequestsPerMinute uint16 `split_words:"true" required:"true"`
|
||||||
ArbitrumBaseUrl string `split_words:"true" required:"true"`
|
ArbitrumBaseUrl string `split_words:"true" required:"true"`
|
||||||
ArbitrumRequestsPerMinute uint16 `split_words:"true" required:"true"`
|
ArbitrumRequestsPerMinute uint16 `split_words:"true" required:"true"`
|
||||||
AvalancheBaseUrl string `split_words:"true" required:"true"`
|
AvalancheBaseUrl string `split_words:"true" required:"true"`
|
||||||
|
|
Loading…
Reference in New Issue