[API] Standard payload changes (#520)

### Summary

This pull request modifies the API service to expose two endpoints for the Wormhole Scan UI:
* `GET /api/v1/transactions`: data needed to render the transactions list page.
* `GET /api/v1/transactions/{chain}/{id}/{sequence}`: data needed to render the transaction detail page.
This commit is contained in:
agodnic 2023-07-12 12:51:52 -03:00 committed by GitHub
parent 4f1987c698
commit fe196e35f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 596 additions and 273 deletions

View File

@ -1077,6 +1077,52 @@ const docTemplate = `{
}
}
},
"/api/v1/transactions/{chain_id}/{emitter}/{seq}": {
"get": {
"description": "Find VAA metadata by ID.",
"tags": [
"Wormscan"
],
"operationId": "get-transaction-by-id",
"parameters": [
{
"type": "integer",
"description": "id of the blockchain",
"name": "chain_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "address of the emitter",
"name": "emitter",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "sequence of the VAA",
"name": "seq",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/transactions.TransactionDetail"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/vaas/": {
"get": {
"description": "Returns all VAAs. Output is paginated and can also be be sorted.",
@ -1734,52 +1780,6 @@ const docTemplate = `{
}
}
},
"github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview": {
"type": "object",
"properties": {
"destinationAddress": {
"type": "string"
},
"destinationChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"emitterAddress": {
"description": "EmitterAddress contains the VAA's emitter address, encoded in hex.",
"type": "string"
},
"emitterNativeAddress": {
"description": "EmitterNativeAddress contains the VAA's emitter address, encoded in the emitter chain's native format.",
"type": "string"
},
"id": {
"type": "string"
},
"originAddress": {
"type": "string"
},
"originChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"status": {
"$ref": "#/definitions/transactions.TxStatus"
},
"symbol": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"tokenAmount": {
"type": "string"
},
"txHash": {
"type": "string"
},
"usdAmount": {
"type": "string"
}
}
},
"governor.AvailableNotionalItemResponse": {
"type": "object",
"properties": {
@ -2531,17 +2531,83 @@ const docTemplate = `{
}
}
},
"transactions.DestinationTx": {
"type": "object",
"properties": {
"blockNumber": {
"type": "string"
},
"chainId": {
"$ref": "#/definitions/vaa.ChainID"
},
"from": {
"type": "string"
},
"method": {
"type": "string"
},
"status": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"to": {
"type": "string"
},
"txHash": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"transactions.GlobalTransactionDoc": {
"type": "object",
"properties": {
"destinationTx": {
"$ref": "#/definitions/transactions.DestinationTx"
},
"id": {
"type": "string"
},
"originTx": {
"$ref": "#/definitions/transactions.OriginTx"
}
}
},
"transactions.ListTransactionsResponse": {
"type": "object",
"properties": {
"transactions": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview"
"$ref": "#/definitions/transactions.TransactionDetail"
}
}
}
},
"transactions.OriginTx": {
"type": "object",
"properties": {
"chainId": {
"$ref": "#/definitions/vaa.ChainID"
},
"from": {
"type": "string"
},
"status": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"txHash": {
"type": "string"
}
}
},
"transactions.ScorecardsResponse": {
"type": "object",
"properties": {
@ -2617,6 +2683,51 @@ const docTemplate = `{
}
}
},
"transactions.TransactionDetail": {
"type": "object",
"properties": {
"emitterAddress": {
"description": "EmitterAddress contains the VAA's emitter address, encoded in hex.",
"type": "string"
},
"emitterChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"emitterNativeAddress": {
"description": "EmitterNativeAddress contains the VAA's emitter address, encoded in the emitter chain's native format.",
"type": "string"
},
"globalTx": {
"$ref": "#/definitions/transactions.GlobalTransactionDoc"
},
"id": {
"type": "string"
},
"payload": {
"type": "object",
"additionalProperties": true
},
"standardizedProperties": {
"type": "object",
"additionalProperties": true
},
"symbol": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"tokenAmount": {
"type": "string"
},
"txHash": {
"type": "string"
},
"usdAmount": {
"type": "string"
}
}
},
"transactions.Tx": {
"type": "object",
"properties": {
@ -2637,17 +2748,6 @@ const docTemplate = `{
}
}
},
"transactions.TxStatus": {
"type": "string",
"enum": [
"ongoing",
"completed"
],
"x-enum-varnames": [
"TxStatusOngoing",
"TxStatusCompleted"
]
},
"vaa.ChainID": {
"type": "integer",
"enum": [

View File

@ -1070,6 +1070,52 @@
}
}
},
"/api/v1/transactions/{chain_id}/{emitter}/{seq}": {
"get": {
"description": "Find VAA metadata by ID.",
"tags": [
"Wormscan"
],
"operationId": "get-transaction-by-id",
"parameters": [
{
"type": "integer",
"description": "id of the blockchain",
"name": "chain_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "address of the emitter",
"name": "emitter",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "sequence of the VAA",
"name": "seq",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/transactions.TransactionDetail"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/vaas/": {
"get": {
"description": "Returns all VAAs. Output is paginated and can also be be sorted.",
@ -1727,52 +1773,6 @@
}
}
},
"github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview": {
"type": "object",
"properties": {
"destinationAddress": {
"type": "string"
},
"destinationChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"emitterAddress": {
"description": "EmitterAddress contains the VAA's emitter address, encoded in hex.",
"type": "string"
},
"emitterNativeAddress": {
"description": "EmitterNativeAddress contains the VAA's emitter address, encoded in the emitter chain's native format.",
"type": "string"
},
"id": {
"type": "string"
},
"originAddress": {
"type": "string"
},
"originChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"status": {
"$ref": "#/definitions/transactions.TxStatus"
},
"symbol": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"tokenAmount": {
"type": "string"
},
"txHash": {
"type": "string"
},
"usdAmount": {
"type": "string"
}
}
},
"governor.AvailableNotionalItemResponse": {
"type": "object",
"properties": {
@ -2524,17 +2524,83 @@
}
}
},
"transactions.DestinationTx": {
"type": "object",
"properties": {
"blockNumber": {
"type": "string"
},
"chainId": {
"$ref": "#/definitions/vaa.ChainID"
},
"from": {
"type": "string"
},
"method": {
"type": "string"
},
"status": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"to": {
"type": "string"
},
"txHash": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"transactions.GlobalTransactionDoc": {
"type": "object",
"properties": {
"destinationTx": {
"$ref": "#/definitions/transactions.DestinationTx"
},
"id": {
"type": "string"
},
"originTx": {
"$ref": "#/definitions/transactions.OriginTx"
}
}
},
"transactions.ListTransactionsResponse": {
"type": "object",
"properties": {
"transactions": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview"
"$ref": "#/definitions/transactions.TransactionDetail"
}
}
}
},
"transactions.OriginTx": {
"type": "object",
"properties": {
"chainId": {
"$ref": "#/definitions/vaa.ChainID"
},
"from": {
"type": "string"
},
"status": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"txHash": {
"type": "string"
}
}
},
"transactions.ScorecardsResponse": {
"type": "object",
"properties": {
@ -2610,6 +2676,51 @@
}
}
},
"transactions.TransactionDetail": {
"type": "object",
"properties": {
"emitterAddress": {
"description": "EmitterAddress contains the VAA's emitter address, encoded in hex.",
"type": "string"
},
"emitterChain": {
"$ref": "#/definitions/vaa.ChainID"
},
"emitterNativeAddress": {
"description": "EmitterNativeAddress contains the VAA's emitter address, encoded in the emitter chain's native format.",
"type": "string"
},
"globalTx": {
"$ref": "#/definitions/transactions.GlobalTransactionDoc"
},
"id": {
"type": "string"
},
"payload": {
"type": "object",
"additionalProperties": true
},
"standardizedProperties": {
"type": "object",
"additionalProperties": true
},
"symbol": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"tokenAmount": {
"type": "string"
},
"txHash": {
"type": "string"
},
"usdAmount": {
"type": "string"
}
}
},
"transactions.Tx": {
"type": "object",
"properties": {
@ -2630,17 +2741,6 @@
}
}
},
"transactions.TxStatus": {
"type": "string",
"enum": [
"ongoing",
"completed"
],
"x-enum-varnames": [
"TxStatusOngoing",
"TxStatusCompleted"
]
},
"vaa.ChainID": {
"type": "integer",
"enum": [

View File

@ -16,39 +16,6 @@ definitions:
index:
type: integer
type: object
github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview:
properties:
destinationAddress:
type: string
destinationChain:
$ref: '#/definitions/vaa.ChainID'
emitterAddress:
description: EmitterAddress contains the VAA's emitter address, encoded in
hex.
type: string
emitterNativeAddress:
description: EmitterNativeAddress contains the VAA's emitter address, encoded
in the emitter chain's native format.
type: string
id:
type: string
originAddress:
type: string
originChain:
$ref: '#/definitions/vaa.ChainID'
status:
$ref: '#/definitions/transactions.TxStatus'
symbol:
type: string
timestamp:
type: string
tokenAmount:
type: string
txHash:
type: string
usdAmount:
type: string
type: object
governor.AvailableNotionalItemResponse:
properties:
bigTransactionSize:
@ -535,13 +502,56 @@ definitions:
volume:
type: number
type: object
transactions.DestinationTx:
properties:
blockNumber:
type: string
chainId:
$ref: '#/definitions/vaa.ChainID'
from:
type: string
method:
type: string
status:
type: string
timestamp:
type: string
to:
type: string
txHash:
type: string
updatedAt:
type: string
type: object
transactions.GlobalTransactionDoc:
properties:
destinationTx:
$ref: '#/definitions/transactions.DestinationTx'
id:
type: string
originTx:
$ref: '#/definitions/transactions.OriginTx'
type: object
transactions.ListTransactionsResponse:
properties:
transactions:
items:
$ref: '#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview'
$ref: '#/definitions/transactions.TransactionDetail'
type: array
type: object
transactions.OriginTx:
properties:
chainId:
$ref: '#/definitions/vaa.ChainID'
from:
type: string
status:
type: string
timestamp:
type: string
txHash:
type: string
type: object
transactions.ScorecardsResponse:
properties:
24h_messages:
@ -595,6 +605,39 @@ definitions:
time:
type: string
type: object
transactions.TransactionDetail:
properties:
emitterAddress:
description: EmitterAddress contains the VAA's emitter address, encoded in
hex.
type: string
emitterChain:
$ref: '#/definitions/vaa.ChainID'
emitterNativeAddress:
description: EmitterNativeAddress contains the VAA's emitter address, encoded
in the emitter chain's native format.
type: string
globalTx:
$ref: '#/definitions/transactions.GlobalTransactionDoc'
id:
type: string
payload:
additionalProperties: true
type: object
standardizedProperties:
additionalProperties: true
type: object
symbol:
type: string
timestamp:
type: string
tokenAmount:
type: string
txHash:
type: string
usdAmount:
type: string
type: object
transactions.Tx:
properties:
chain:
@ -608,14 +651,6 @@ definitions:
volume:
type: number
type: object
transactions.TxStatus:
enum:
- ongoing
- completed
type: string
x-enum-varnames:
- TxStatusOngoing
- TxStatusCompleted
vaa.ChainID:
enum:
- 0
@ -1455,6 +1490,37 @@ paths:
description: Internal Server Error
tags:
- Wormscan
/api/v1/transactions/{chain_id}/{emitter}/{seq}:
get:
description: Find VAA metadata by ID.
operationId: get-transaction-by-id
parameters:
- description: id of the blockchain
in: path
name: chain_id
required: true
type: integer
- description: address of the emitter
in: path
name: emitter
required: true
type: string
- description: sequence of the VAA
in: path
name: seq
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/transactions.TransactionDetail'
"400":
description: Bad Request
"500":
description: Internal Server Error
tags:
- Wormscan
/api/v1/vaas/:
get:
description: Returns all VAAs. Output is paginated and can also be be sorted.

View File

@ -181,22 +181,16 @@ type Token struct {
Decimals int64 `json:"decimals"`
}
// TransactionOverview models a brief overview of a transactions (ID, txHash, status, etc.)
type TransactionOverview struct {
ID string `bson:"_id"`
EmitterChain sdk.ChainID `bson:"emitterChain"`
EmitterAddr string `bson:"emitterAddr"`
TxHash string `bson:"txHash"`
Timestamp time.Time `bson:"timestamp"`
ToAddress string `bson:"toAddress"`
ToChain sdk.ChainID `bson:"toChain"`
Symbol string `bson:"symbol"`
UsdAmount string `bson:"usdAmount"`
TokenAmount string `bson:"tokenAmount"`
GlobalTransations []GlobalTransactionDoc `bson:"globalTransactions"`
}
// ListTransactionsInput is used as the output for the function `ListTransactions`
type ListTransactonsOutput struct {
Transactions []TransactionOverview
type TransactionDto struct {
ID string `bson:"_id"`
EmitterChain sdk.ChainID `bson:"emitterChain"`
EmitterAddr string `bson:"emitterAddr"`
TxHash string `bson:"txHash"`
Timestamp time.Time `bson:"timestamp"`
Symbol string `bson:"symbol"`
UsdAmount string `bson:"usdAmount"`
TokenAmount string `bson:"tokenAmount"`
GlobalTransations []GlobalTransactionDoc `bson:"globalTransactions"`
Payload map[string]interface{} `bson:"payload"`
StandardizedProperties map[string]interface{} `bson:"standardizedProperties"`
}

View File

@ -726,24 +726,43 @@ func (r *Repository) findGlobalTransactionByID(ctx context.Context, q *GlobalTra
return &globalTranstaction, nil
}
// ListTransactions returns a sorted list of transactions.
//
// Pagination is implemented using a keyset cursor pattern, based on the (timestamp, ID) pair.
func (r *Repository) ListTransactions(
// FindTransactionsInput is used to pass parameters to the `FindTransactions` method.
type FindTransactionsInput struct {
// id specifies the VAA ID of the transaction to be found.
id string
// sort specifies whether the results should be sorted
//
// If set to true, the results will be sorted by descending timestamp and ID.
// If set to false, the results will not be sorted.
sort bool
pagination *pagination.Pagination
}
// FindTransactions returns transactions matching a specified search criteria.
func (r *Repository) FindTransactions(
ctx context.Context,
pagination *pagination.Pagination,
) (*ListTransactonsOutput, error) {
input *FindTransactionsInput,
) ([]TransactionDto, error) {
// Build the aggregation pipeline
var pipeline mongo.Pipeline
{
// Specify sorting criteria
pipeline = append(pipeline, bson.D{
{"$sort", bson.D{
bson.E{"timestamp", -1},
bson.E{"_id", -1},
}},
})
if input.sort {
pipeline = append(pipeline, bson.D{
{"$sort", bson.D{
bson.E{"timestamp", -1},
bson.E{"_id", -1},
}},
})
}
// Filter by ID
if input.id != "" {
pipeline = append(pipeline, bson.D{
{"$match", bson.D{{"_id", input.id}}},
})
}
// left outer join on the `transferPrices` collection
pipeline = append(pipeline, bson.D{
@ -789,8 +808,8 @@ func (r *Repository) ListTransactions(
pipeline = append(pipeline, bson.D{
{"$addFields", bson.D{
{"txHash", bson.M{"$arrayElemAt": []interface{}{"$vaaIdTxHash.txHash", 0}}},
{"toAddress", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.result.toAddress", 0}}},
{"toChain", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.result.toChain", 0}}},
{"payload", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.parsedPayload", 0}}},
{"standardizedProperties", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.standardizedProperties", 0}}},
{"symbol", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.symbol", 0}}},
{"usdAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.usdAmount", 0}}},
{"tokenAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.tokenAmount", 0}}},
@ -803,14 +822,18 @@ func (r *Repository) ListTransactions(
})
// Skip initial results
pipeline = append(pipeline, bson.D{
{"$skip", pagination.Skip},
})
if input.pagination != nil {
pipeline = append(pipeline, bson.D{
{"$skip", input.pagination.Skip},
})
}
// Limit size of results
pipeline = append(pipeline, bson.D{
{"$limit", pagination.Limit},
})
if input.pagination != nil {
pipeline = append(pipeline, bson.D{
{"$limit", input.pagination.Limit},
})
}
}
// Execute the aggregation pipeline
@ -821,18 +844,14 @@ func (r *Repository) ListTransactions(
}
// Read results from cursor
var documents []TransactionOverview
var documents []TransactionDto
err = cur.All(ctx, &documents)
if err != nil {
r.logger.Error("failed to decode cursor", zap.Error(err))
return nil, err
}
// Build result and return
response := ListTransactonsOutput{
Transactions: documents,
}
return &response, nil
return documents, nil
}
// ListTransactionsByAddress returns a sorted list of transactions for a given address.
@ -842,14 +861,14 @@ func (r *Repository) ListTransactionsByAddress(
ctx context.Context,
address *types.Address,
pagination *pagination.Pagination,
) (*ListTransactonsOutput, error) {
) ([]TransactionDto, error) {
// Build the aggregation pipeline
var pipeline mongo.Pipeline
{
// filter by address
pipeline = append(pipeline, bson.D{
{"$match", bson.D{{"result.toAddress", bson.M{"$eq": "0x" + address.Hex()}}}},
{"$match", bson.D{{"parsedPayload.toAddress", bson.M{"$eq": "0x" + address.Hex()}}}},
})
// specify sorting criteria
@ -912,8 +931,8 @@ func (r *Repository) ListTransactionsByAddress(
{"$addFields", bson.D{
{"txHash", bson.M{"$arrayElemAt": []interface{}{"$vaaIdTxHash.txHash", 0}}},
{"timestamp", bson.M{"$arrayElemAt": []interface{}{"$vaas.timestamp", 0}}},
{"toAddress", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.result.toAddress", 0}}},
{"toChain", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.result.toChain", 0}}},
{"payload", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.parsedPayload", 0}}},
{"standardizedProperties", bson.M{"$arrayElemAt": []interface{}{"$parsedVaa.standardizedProperties", 0}}},
{"symbol", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.symbol", 0}}},
{"usdAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.usdAmount", 0}}},
{"tokenAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.tokenAmount", 0}}},
@ -944,16 +963,12 @@ func (r *Repository) ListTransactionsByAddress(
}
// Read results from cursor
var documents []TransactionOverview
var documents []TransactionDto
err = cur.All(ctx, &documents)
if err != nil {
r.logger.Error("failed to decode cursor", zap.Error(err))
return nil, err
}
// Build result and return
response := ListTransactonsOutput{
Transactions: documents,
}
return &response, nil
return documents, nil
}

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/wormhole-foundation/wormhole-explorer/api/cacheable"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
"github.com/wormhole-foundation/wormhole-explorer/api/types"
@ -118,16 +119,43 @@ func (s *Service) GetTokenByChainAndAddress(ctx context.Context, chainID vaa.Cha
func (s *Service) ListTransactions(
ctx context.Context,
pagination *pagination.Pagination,
) (*ListTransactonsOutput, error) {
) ([]TransactionDto, error) {
return s.repo.ListTransactions(ctx, pagination)
input := FindTransactionsInput{
sort: true,
pagination: pagination,
}
return s.repo.FindTransactions(ctx, &input)
}
func (s *Service) ListTransactionsByAddress(
ctx context.Context,
address *types.Address,
pagination *pagination.Pagination,
) (*ListTransactonsOutput, error) {
) ([]TransactionDto, error) {
return s.repo.ListTransactionsByAddress(ctx, address, pagination)
}
func (s *Service) GetTransactionByID(
ctx context.Context,
chain vaa.ChainID,
emitter *types.Address,
seq string,
) (*TransactionDto, error) {
// Execute the database query
input := FindTransactionsInput{
id: fmt.Sprintf("%d/%s/%s", chain, emitter.Hex(), seq),
}
output, err := s.repo.FindTransactions(ctx, &input)
if err != nil {
return nil, err
}
if len(output) == 0 {
return nil, errors.ErrNotFound
}
// Return matching document
return &output[0], nil
}

View File

@ -71,6 +71,7 @@ func RegisterRoutes(
api.Get("/top-chain-pairs-by-num-transfers", transactionCtrl.GetTopChainPairs)
api.Get("token/:chain/:token_address", transactionCtrl.GetTokenByChainAndAddress)
api.Get("/transactions", transactionCtrl.ListTransactions)
api.Get("/transactions/:chain/:emitter/:sequence", transactionCtrl.GetTransactionByID)
// vaas resource
vaas := api.Group("/vaas")

View File

@ -6,6 +6,7 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
"github.com/wormhole-foundation/wormhole-explorer/api/middleware"
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
@ -370,55 +371,55 @@ func (c *Controller) ListTransactions(ctx *fiber.Ctx) error {
}
// Query transactions from the database
var queryResult *transactions.ListTransactonsOutput
var dtos []transactions.TransactionDto
if address != nil {
queryResult, err = c.srv.ListTransactionsByAddress(ctx.Context(), address, pagination)
dtos, err = c.srv.ListTransactionsByAddress(ctx.Context(), address, pagination)
} else {
queryResult, err = c.srv.ListTransactions(ctx.Context(), pagination)
dtos, err = c.srv.ListTransactions(ctx.Context(), pagination)
}
if err != nil {
return err
}
// Populate the response struct and return
response := c.makeTransactionsResponse(queryResult)
response := c.makeTransactionsResponse(dtos)
return ctx.JSON(response)
}
func (c *Controller) makeTransactionsResponse(queryResult *transactions.ListTransactonsOutput) ListTransactionsResponse {
func (c *Controller) makeTransactionsResponse(dtos []transactions.TransactionDto) ListTransactionsResponse {
response := ListTransactionsResponse{
Transactions: make([]*TransactionOverview, 0, len(queryResult.Transactions)),
Transactions: make([]*TransactionDetail, 0, len(dtos)),
}
for i := range queryResult.Transactions {
tx := c.makeTransactionOverview(&queryResult.Transactions[i])
for i := range dtos {
tx := c.makeTransactionDetail(&dtos[i])
response.Transactions = append(response.Transactions, tx)
}
return response
}
func (c *Controller) makeTransactionOverview(input *transactions.TransactionOverview) *TransactionOverview {
func (c *Controller) makeTransactionDetail(input *transactions.TransactionDto) *TransactionDetail {
tx := TransactionOverview{
ID: input.ID,
OriginChain: input.EmitterChain,
EmitterAddress: input.EmitterAddr,
Timestamp: input.Timestamp,
DestinationAddress: input.ToAddress,
DestinationChain: input.ToChain,
Symbol: input.Symbol,
TokenAmount: input.TokenAmount,
UsdAmount: input.UsdAmount,
tx := TransactionDetail{
ID: input.ID,
EmitterChain: input.EmitterChain,
EmitterAddress: input.EmitterAddr,
Timestamp: input.Timestamp,
Symbol: input.Symbol,
TokenAmount: input.TokenAmount,
UsdAmount: input.UsdAmount,
Payload: input.Payload,
StandardizedProperties: input.StandardizedProperties,
}
// Translate the emitter address into the emitter chain's native format
var err error
tx.EmitterNativeAddress, err = domain.TranslateEmitterAddress(tx.OriginChain, tx.EmitterAddress)
tx.EmitterNativeAddress, err = domain.TranslateEmitterAddress(tx.EmitterChain, tx.EmitterAddress)
if err != nil {
c.logger.Warn("failed to translate emitter address",
zap.Stringer("chain", tx.OriginChain),
zap.Stringer("chain", tx.EmitterChain),
zap.String("address", tx.EmitterAddress),
zap.Error(err),
)
@ -429,29 +430,54 @@ func (c *Controller) makeTransactionOverview(input *transactions.TransactionOver
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(input.GlobalTransations) == 1 &&
input.GlobalTransations[0].OriginTx != nil {
if len(input.GlobalTransations) == 1 && input.GlobalTransations[0].OriginTx != nil {
tx.TxHash = input.GlobalTransations[0].OriginTx.TxHash
}
} else {
tx.TxHash = input.TxHash
}
// Set the status based on the outcome of the redeem transaction.
if len(input.GlobalTransations) == 1 &&
input.GlobalTransations[0].DestinationTx != nil &&
input.GlobalTransations[0].DestinationTx.Status == domain.DstTxStatusConfirmed {
tx.Status = TxStatusCompleted
} else {
tx.Status = TxStatusOngoing
}
// Set the origin address, if available
// Set the global transaction, if available
if len(input.GlobalTransations) == 1 && input.GlobalTransations[0].OriginTx != nil {
tx.OriginAddress = input.GlobalTransations[0].OriginTx.From
tx.GlobalTx = &input.GlobalTransations[0]
}
return &tx
}
// GetTransactionByID godoc
// @Description Find VAA metadata by ID.
// @Tags Wormscan
// @ID get-transaction-by-id
// @Param chain_id path integer true "id of the blockchain"
// @Param emitter path string true "address of the emitter"
// @Param seq path integer true "sequence of the VAA"
// @Success 200 {object} TransactionDetail
// @Failure 400
// @Failure 500
// @Router /api/v1/transactions/{chain_id}/{emitter}/{seq} [get]
func (c *Controller) GetTransactionByID(ctx *fiber.Ctx) error {
// Extract query params
chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger)
if err != nil {
return err
}
// Look up the VAA by ID
dto, err := c.srv.GetTransactionByID(
ctx.Context(),
chainID,
emitter,
strconv.FormatUint(seq, 10),
)
if err != nil {
return err
}
if dto == nil {
return errors.ErrNotFound
}
tx := c.makeTransactionDetail(dto)
return ctx.JSON(tx)
}

View File

@ -3,36 +3,29 @@ package transactions
import (
"time"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
)
type TxStatus string
const (
TxStatusOngoing TxStatus = "ongoing"
TxStatusCompleted TxStatus = "completed"
)
// TransactionOverview is a brief description of a transaction (e.g. ID, txHash, status, etc.).
type TransactionOverview struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
TxHash string `json:"txHash,omitempty"`
OriginAddress string `json:"originAddress,omitempty"`
OriginChain sdk.ChainID `json:"originChain"`
// TransactionDetail is a brief description of a transaction (e.g. ID, txHash, payload, etc.)
type TransactionDetail struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
TxHash string `json:"txHash,omitempty"`
EmitterChain sdk.ChainID `json:"emitterChain"`
// EmitterAddress contains the VAA's emitter address, encoded in hex.
EmitterAddress string `json:"emitterAddress"`
// EmitterNativeAddress contains the VAA's emitter address, encoded in the emitter chain's native format.
EmitterNativeAddress string `json:"emitterNativeAddress,omitempty"`
DestinationAddress string `json:"destinationAddress,omitempty"`
DestinationChain sdk.ChainID `json:"destinationChain,omitempty"`
TokenAmount string `json:"tokenAmount,omitempty"`
UsdAmount string `json:"usdAmount,omitempty"`
Symbol string `json:"symbol,omitempty"`
Status TxStatus `json:"status"`
EmitterNativeAddress string `json:"emitterNativeAddress,omitempty"`
TokenAmount string `json:"tokenAmount,omitempty"`
UsdAmount string `json:"usdAmount,omitempty"`
Symbol string `json:"symbol,omitempty"`
Payload map[string]interface{} `json:"payload,omitempty"`
StandardizedProperties map[string]interface{} `json:"standardizedProperties,omitempty"`
GlobalTx *transactions.GlobalTransactionDoc `json:"globalTx,omitempty"`
}
// ListTransactionsResponse is the "200 OK" response model for `GET /api/v1/transactions`.
type ListTransactionsResponse struct {
Transactions []*TransactionOverview `json:"transactions"`
Transactions []*TransactionDetail `json:"transactions"`
}