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:
agodnic 2023-06-16 17:47:28 -03:00 committed by GitHub
parent ba3d1d9e61
commit 3f823c51ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 9 deletions

View File

@ -399,14 +399,17 @@ func (c *Controller) ListTransactions(ctx *fiber.Ctx) error {
UsdAmount: queryResult.Transactions[i].UsdAmount,
}
// For Solana VAAs, the txHash that we get from the gossip network is not the real transacion hash,
// so we have to overwrite it with the real txHash.
if queryResult.Transactions[i].EmitterChain == sdk.ChainIDSolana &&
len(queryResult.Transactions[i].GlobalTransations) == 1 &&
queryResult.Transactions[i].GlobalTransations[0].OriginTx != nil {
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash
// Set the transaction hash
isSolanaOrAptos := queryResult.Transactions[i].EmitterChain == sdk.ChainIDSolana ||
queryResult.Transactions[i].EmitterChain == sdk.ChainIDAptos
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 {
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash
}
} else {
tx.TxHash = queryResult.Transactions[i].TxHash
}

View File

@ -31,6 +31,10 @@ spec:
configMapKeyRef:
name: config
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
value: {{ .ARBITRUM_BASE_URL }}
- name: ARBITRUM_REQUESTS_PER_MINUTE

View File

@ -57,6 +57,10 @@ spec:
value: {{ .SQS_URL }}
- name: 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
value: {{ .ARBITRUM_BASE_URL }}
- name: ARBITRUM_REQUESTS_PER_MINUTE

View File

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

View File

@ -4,7 +4,9 @@ import (
"context"
"errors"
"fmt"
"io/ioutil"
"math"
"net/http"
"strconv"
"strings"
"time"
@ -39,6 +41,7 @@ var tickers = struct {
optimism *time.Ticker
polygon *time.Ticker
solana *time.Ticker
aptos *time.Ticker
sui *time.Ticker
}{}
@ -57,6 +60,7 @@ func Initialize(cfg *config.RpcProviderSettings) {
tickers.sui = time.NewTicker(f(cfg.SuiRequestsPerMinute))
// these adapters send 2 requests per txHash
tickers.aptos = time.NewTicker(f(cfg.AptosRequestsPerMinute / 2))
tickers.arbitrum = time.NewTicker(f(cfg.ArbitrumRequestsPerMinute / 2))
tickers.avalanche = time.NewTicker(f(cfg.AvalancheRequestsPerMinute / 2))
tickers.bsc = time.NewTicker(f(cfg.BscRequestsPerMinute / 2))
@ -123,6 +127,9 @@ func FetchTx(
return fetchEthTx(ctx, txHash, cfg.AvalancheBaseUrl)
}
rateLimiter = *tickers.avalanche
case vaa.ChainIDAptos:
fetchFunc = fetchAptosTx
rateLimiter = *tickers.aptos
case vaa.ChainIDSui:
fetchFunc = fetchSuiTx
rateLimiter = *tickers.sui
@ -165,3 +172,28 @@ func timestampFromHex(s string) (time.Time, error) {
timestamp := time.Unix(epoch, 0).UTC()
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
}

View File

@ -37,6 +37,5 @@ func main() {
}
// print tx details
log.Printf("tx detail: sender=%s nativeTxHash=%s timestamp=%s",
txDetail.From, txDetail.NativeTxHash, txDetail.Timestamp)
log.Printf("tx detail: %+v", txDetail)
}

View File

@ -59,6 +59,8 @@ type MongodbSettings 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"`
ArbitrumRequestsPerMinute uint16 `split_words:"true" required:"true"`
AvalancheBaseUrl string `split_words:"true" required:"true"`