diff --git a/api/routes/wormscan/transactions/controller.go b/api/routes/wormscan/transactions/controller.go index 6c1f5563..f35329f9 100644 --- a/api/routes/wormscan/transactions/controller.go +++ b/api/routes/wormscan/transactions/controller.go @@ -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 } diff --git a/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml b/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml index 9a628d4c..1d0cf3b8 100644 --- a/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml +++ b/deploy/tx-tracker-backfiller/tx-tracker-backfiller-job.yaml @@ -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 diff --git a/deploy/tx-tracker/tx-tracker-service.yaml b/deploy/tx-tracker/tx-tracker-service.yaml index 6b65b4fe..4049d25b 100644 --- a/deploy/tx-tracker/tx-tracker-service.yaml +++ b/deploy/tx-tracker/tx-tracker-service.yaml @@ -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 diff --git a/tx-tracker/chains/aptos.go b/tx-tracker/chains/aptos.go new file mode 100644 index 00000000..76f15f66 --- /dev/null +++ b/tx-tracker/chains/aptos.go @@ -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 +} diff --git a/tx-tracker/chains/tx.go b/tx-tracker/chains/tx.go index d1bfc30a..1d648f76 100644 --- a/tx-tracker/chains/tx.go +++ b/tx-tracker/chains/tx.go @@ -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 +} diff --git a/tx-tracker/cmd/fetchone/main.go b/tx-tracker/cmd/fetchone/main.go index 8d451a37..f48e8f58 100644 --- a/tx-tracker/cmd/fetchone/main.go +++ b/tx-tracker/cmd/fetchone/main.go @@ -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) } diff --git a/tx-tracker/config/structs.go b/tx-tracker/config/structs.go index d4f1aaba..ea65e6dd 100644 --- a/tx-tracker/config/structs.go +++ b/tx-tracker/config/structs.go @@ -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"`