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, 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 {
queryResult.Transactions[i].GlobalTransations[0].OriginTx != nil { // 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.
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash if len(queryResult.Transactions[i].GlobalTransations) == 1 &&
queryResult.Transactions[i].GlobalTransations[0].OriginTx != nil {
tx.TxHash = queryResult.Transactions[i].GlobalTransations[0].OriginTx.TxHash
}
} else { } else {
tx.TxHash = queryResult.Transactions[i].TxHash tx.TxHash = queryResult.Transactions[i].TxHash
} }

View File

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

View File

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

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

View File

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

View File

@ -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"`