From fe196e35f0a1eb4afd0ca60e1fbae93cf862e46f Mon Sep 17 00:00:00 2001 From: agodnic Date: Wed, 12 Jul 2023 12:51:52 -0300 Subject: [PATCH] [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. --- api/docs/docs.go | 216 +++++++++++++----- api/docs/swagger.json | 216 +++++++++++++----- api/docs/swagger.yaml | 150 ++++++++---- api/handlers/transactions/model.go | 30 +-- api/handlers/transactions/repository.go | 87 ++++--- api/handlers/transactions/service.go | 34 ++- api/routes/wormscan/routes.go | 1 + .../wormscan/transactions/controller.go | 98 +++++--- api/routes/wormscan/transactions/models.go | 37 ++- 9 files changed, 596 insertions(+), 273 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 0f2242bd..6999b136 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -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": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index fd3363d1..e0417cd5 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -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": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 9f47ee04..4e72fc05 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -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. diff --git a/api/handlers/transactions/model.go b/api/handlers/transactions/model.go index 859d1d7d..7d1e9a47 100644 --- a/api/handlers/transactions/model.go +++ b/api/handlers/transactions/model.go @@ -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"` } diff --git a/api/handlers/transactions/repository.go b/api/handlers/transactions/repository.go index dba77258..f9d970b7 100644 --- a/api/handlers/transactions/repository.go +++ b/api/handlers/transactions/repository.go @@ -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 } diff --git a/api/handlers/transactions/service.go b/api/handlers/transactions/service.go index 1479c265..7e071d08 100644 --- a/api/handlers/transactions/service.go +++ b/api/handlers/transactions/service.go @@ -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 +} diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go index f42a923a..0f2b084c 100644 --- a/api/routes/wormscan/routes.go +++ b/api/routes/wormscan/routes.go @@ -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") diff --git a/api/routes/wormscan/transactions/controller.go b/api/routes/wormscan/transactions/controller.go index eb884de7..075a7cb0 100644 --- a/api/routes/wormscan/transactions/controller.go +++ b/api/routes/wormscan/transactions/controller.go @@ -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) +} diff --git a/api/routes/wormscan/transactions/models.go b/api/routes/wormscan/transactions/models.go index 4480f0ec..e27d48cc 100644 --- a/api/routes/wormscan/transactions/models.go +++ b/api/routes/wormscan/transactions/models.go @@ -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"` }