Add endpoint `GET /api/v1/transactions` (#388)
### Summary Tracking issue: https://github.com/wormhole-foundation/wormhole-explorer/issues/385 This pull request implements a new endpoint, `GET /api/v1/transactions`, which will be consumed by the wormhole explorer UI. The endpoint returns a paginated list of transactions, in which each element contains a brief overview of the transaction (ID, txHash, status, etc.). It exposes offset-based pagination via the parameters `page` and `pageSize`. Also, results can be obtained for a specific address by using the `address` query parameter. The response model looks like this: ```json { "transactions": [ { "id": "1/5ec18c34b47c63d17ab43b07b9b2319ea5ee2d163bce2e467000174e238c8e7f/12965", "timestamp": "2023-06-08T19:30:19Z", "txHash": "a302c4ab2d6b9a6003951d2e91f8fdbb83cfa20f6ffb588b95ef0290aab37066", "originChain": 1, "status": "ongoing" }, { "id": "22/0000000000000000000000000000000000000000000000000000000000000001/18308", "timestamp": "2023-06-08T19:17:14Z", "txHash": "00000000000000000000000000000000000000000000000000000000000047e7", "originChain": 22, "destinationAddress": "0x00000000000000000000000067e8a40816a983fbe3294aaebd0cc2391815b86b", "destinationChain": 5, "tokenAmount": "0.12", "usdAmount": "0.12012", "symbol": "USDC", "status": "completed" }, ... ] } ``` ### Limitations of the current implementation 1. Doesn't return the total number of results (this may result in a performance issue when we filter by address) 2. Can only filter by receiver address (we don't have sender information in the database yet)
This commit is contained in:
parent
d0436fa46d
commit
a0475ab17e
175
api/docs/docs.go
175
api/docs/docs.go
|
@ -1,5 +1,4 @@
|
|||
// Code generated by swaggo/swag. DO NOT EDIT.
|
||||
|
||||
// Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
@ -575,12 +574,19 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -893,12 +899,19 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ready": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ready": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1027,6 +1040,43 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/transactions/": {
|
||||
"get": {
|
||||
"description": "Returns transactions. Output is paginated.",
|
||||
"tags": [
|
||||
"Wormscan"
|
||||
],
|
||||
"operationId": "list-transactions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page number. Starts at 0.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of elements per page.",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/transactions.ListTransactionsResponse"
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
|
@ -1575,15 +1625,22 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1629,15 +1686,22 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1676,6 +1740,41 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"destinationAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"destinationChain": {
|
||||
"$ref": "#/definitions/vaa.ChainID"
|
||||
},
|
||||
"id": {
|
||||
"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": {
|
||||
|
@ -2427,6 +2526,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"transactions.ListTransactionsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"transactions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"transactions.ScorecardsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2522,6 +2632,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"transactions.TxStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ongoing",
|
||||
"completed"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TxStatusOngoing",
|
||||
"TxStatusCompleted"
|
||||
]
|
||||
},
|
||||
"vaa.ChainID": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
@ -2667,8 +2788,6 @@ var SwaggerInfo = &swag.Spec{
|
|||
Description: "Wormhole Guardian API\nThis is the API for the Wormhole Guardian and Explorer.\nThe API has two namespaces: wormscan and guardian.\nwormscan is the namespace for the explorer and the new endpoints. The prefix is /api/v1.\nguardian is the legacy namespace backguard compatible with guardian node API. The prefix is /v1.\nThis API is public and does not require authentication although some endpoints are rate limited.\nCheck each endpoint documentation for more information.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -567,12 +567,19 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -885,12 +892,19 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ready": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ready": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1019,6 +1033,43 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/transactions/": {
|
||||
"get": {
|
||||
"description": "Returns transactions. Output is paginated.",
|
||||
"tags": [
|
||||
"Wormscan"
|
||||
],
|
||||
"operationId": "list-transactions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page number. Starts at 0.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of elements per page.",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/transactions.ListTransactionsResponse"
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
|
@ -1567,15 +1618,22 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1621,15 +1679,22 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vaaBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -1668,6 +1733,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"destinationAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"destinationChain": {
|
||||
"$ref": "#/definitions/vaa.ChainID"
|
||||
},
|
||||
"id": {
|
||||
"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": {
|
||||
|
@ -2419,6 +2519,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"transactions.ListTransactionsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"transactions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"transactions.ScorecardsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2514,6 +2625,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"transactions.TxStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ongoing",
|
||||
"completed"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TxStatusOngoing",
|
||||
"TxStatusCompleted"
|
||||
]
|
||||
},
|
||||
"vaa.ChainID": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
|
@ -16,6 +16,29 @@ 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'
|
||||
id:
|
||||
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:
|
||||
|
@ -502,6 +525,13 @@ definitions:
|
|||
volume:
|
||||
type: number
|
||||
type: object
|
||||
transactions.ListTransactionsResponse:
|
||||
properties:
|
||||
transactions:
|
||||
items:
|
||||
$ref: '#/definitions/github_com_wormhole-foundation_wormhole-explorer_api_routes_wormscan_transactions.TransactionOverview'
|
||||
type: array
|
||||
type: object
|
||||
transactions.ScorecardsResponse:
|
||||
properties:
|
||||
24h_messages:
|
||||
|
@ -568,6 +598,14 @@ definitions:
|
|||
volume:
|
||||
type: number
|
||||
type: object
|
||||
transactions.TxStatus:
|
||||
enum:
|
||||
- ongoing
|
||||
- completed
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- TxStatusOngoing
|
||||
- TxStatusCompleted
|
||||
vaa.ChainID:
|
||||
enum:
|
||||
- 0
|
||||
|
@ -1068,10 +1106,12 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
allOf:
|
||||
- type: object
|
||||
- properties:
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
"500":
|
||||
|
@ -1283,10 +1323,12 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
properties:
|
||||
ready:
|
||||
type: string
|
||||
type: object
|
||||
allOf:
|
||||
- type: object
|
||||
- properties:
|
||||
ready:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
"500":
|
||||
|
@ -1381,6 +1423,30 @@ paths:
|
|||
description: Internal Server Error
|
||||
tags:
|
||||
- Wormscan
|
||||
/api/v1/transactions/:
|
||||
get:
|
||||
description: Returns transactions. Output is paginated.
|
||||
operationId: list-transactions
|
||||
parameters:
|
||||
- description: Page number. Starts at 0.
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: Number of elements per page.
|
||||
in: query
|
||||
name: pageSize
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/transactions.ListTransactionsResponse'
|
||||
"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.
|
||||
|
@ -1758,12 +1824,14 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
properties:
|
||||
vaaBytes:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
type: object
|
||||
allOf:
|
||||
- type: object
|
||||
- properties:
|
||||
vaaBytes:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
"500":
|
||||
|
@ -1794,12 +1862,14 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
properties:
|
||||
vaaBytes:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
type: object
|
||||
allOf:
|
||||
- type: object
|
||||
- properties:
|
||||
vaaBytes:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
"500":
|
||||
|
|
|
@ -13,7 +13,9 @@ import (
|
|||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/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/internal/tvl"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/api/types"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
@ -138,6 +140,12 @@ union(tables: [summarized, raw])
|
|||
|> top(columns: ["_value"], n: 7)
|
||||
`
|
||||
|
||||
type repositoryCollections struct {
|
||||
vaas *mongo.Collection
|
||||
parsedVaa *mongo.Collection
|
||||
globalTransactions *mongo.Collection
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
tvl *tvl.Tvl
|
||||
influxCli influxdb2.Client
|
||||
|
@ -146,10 +154,8 @@ type Repository struct {
|
|||
bucket30DaysRetention string
|
||||
bucket24HoursRetention string
|
||||
db *mongo.Database
|
||||
collections struct {
|
||||
globalTransactions *mongo.Collection
|
||||
}
|
||||
logger *zap.Logger
|
||||
collections repositoryCollections
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewRepository(
|
||||
|
@ -169,8 +175,12 @@ func NewRepository(
|
|||
bucket30DaysRetention: bucket30DaysRetention,
|
||||
bucketInfiniteRetention: bucketInfiniteRetention,
|
||||
db: db,
|
||||
collections: struct{ globalTransactions *mongo.Collection }{globalTransactions: db.Collection("globalTransactions")},
|
||||
logger: logger,
|
||||
collections: repositoryCollections{
|
||||
vaas: db.Collection("vaas"),
|
||||
parsedVaa: db.Collection("parsedVaa"),
|
||||
globalTransactions: db.Collection("globalTransactions"),
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return &r
|
||||
|
@ -684,3 +694,254 @@ func (r *Repository) findGlobalTransactionByID(ctx context.Context, q *GlobalTra
|
|||
|
||||
return &globalTranstaction, nil
|
||||
}
|
||||
|
||||
// TransactionOverview models a brief overview of a transactions (ID, txHash, status, etc.)
|
||||
type TransactionOverview struct {
|
||||
ID string `bson:"_id"`
|
||||
EmitterChain sdk.ChainID `bson:"emitterChain"`
|
||||
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
|
||||
}
|
||||
|
||||
// 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(
|
||||
ctx context.Context,
|
||||
pagination *pagination.Pagination,
|
||||
) (*ListTransactonsOutput, 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},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `transferPrices` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "transferPrices"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "transferPrices"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `vaaIdTxHash` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "vaaIdTxHash"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "vaaIdTxHash"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `parsedVaa` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "parsedVaa"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "parsedVaa"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `globalTransactions` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "globalTransactions"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "globalTransactions"},
|
||||
}},
|
||||
})
|
||||
|
||||
// add nested fields
|
||||
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}}},
|
||||
{"symbol", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.symbol", 0}}},
|
||||
{"usdAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.usdAmount", 0}}},
|
||||
{"tokenAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.tokenAmount", 0}}},
|
||||
}},
|
||||
})
|
||||
|
||||
// Unset unused fields
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$unset", []interface{}{"transferPrices", "vaaTxIdHash", "parsedVaa"}},
|
||||
})
|
||||
|
||||
// Skip initial results
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$skip", pagination.Skip},
|
||||
})
|
||||
|
||||
// Limit size of results
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$limit", pagination.Limit},
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the aggregation pipeline
|
||||
cur, err := r.collections.vaas.Aggregate(ctx, pipeline)
|
||||
if err != nil {
|
||||
r.logger.Error("failed execute aggregation pipeline", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read results from cursor
|
||||
var documents []TransactionOverview
|
||||
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
|
||||
}
|
||||
|
||||
// ListTransactionsByAddress returns a sorted list of transactions for a given address.
|
||||
//
|
||||
// Pagination is implemented using a keyset cursor pattern, based on the (timestamp, ID) pair.
|
||||
func (r *Repository) ListTransactionsByAddress(
|
||||
ctx context.Context,
|
||||
address *types.Address,
|
||||
pagination *pagination.Pagination,
|
||||
) (*ListTransactonsOutput, 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()}}}},
|
||||
})
|
||||
|
||||
// specify sorting criteria
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$sort", bson.D{bson.E{"indexedAt", -1}}},
|
||||
})
|
||||
|
||||
// left outer join on the `transferPrices` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "transferPrices"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "transferPrices"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `vaas` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "vaas"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "vaas"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `vaaIdTxHash` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "vaaIdTxHash"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "vaaIdTxHash"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `parsedVaa` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "parsedVaa"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "parsedVaa"},
|
||||
}},
|
||||
})
|
||||
|
||||
// left outer join on the `globalTransactions` collection
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$lookup", bson.D{
|
||||
{"from", "globalTransactions"},
|
||||
{"localField", "_id"},
|
||||
{"foreignField", "_id"},
|
||||
{"as", "globalTransactions"},
|
||||
}},
|
||||
})
|
||||
|
||||
// add nested fields
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$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}}},
|
||||
{"symbol", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.symbol", 0}}},
|
||||
{"usdAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.usdAmount", 0}}},
|
||||
{"tokenAmount", bson.M{"$arrayElemAt": []interface{}{"$transferPrices.tokenAmount", 0}}},
|
||||
}},
|
||||
})
|
||||
|
||||
// Unset unused fields
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$unset", []interface{}{"transferPrices", "vaas", "vaaTxIdHash", "parsedVaa"}},
|
||||
})
|
||||
|
||||
// Skip initial results
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$skip", pagination.Skip},
|
||||
})
|
||||
|
||||
// Limit size of results
|
||||
pipeline = append(pipeline, bson.D{
|
||||
{"$limit", pagination.Limit},
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the aggregation pipeline
|
||||
cur, err := r.collections.parsedVaa.Aggregate(ctx, pipeline)
|
||||
if err != nil {
|
||||
r.logger.Error("failed execute aggregation pipeline", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read results from cursor
|
||||
var documents []TransactionOverview
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
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"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -73,3 +74,20 @@ func (s *Service) GetTokenByChainAndAddress(ctx context.Context, chainID vaa.Cha
|
|||
Decimals: tokenMetadata.Decimals,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListTransactions(
|
||||
ctx context.Context,
|
||||
pagination *pagination.Pagination,
|
||||
) (*ListTransactonsOutput, error) {
|
||||
|
||||
return s.repo.ListTransactions(ctx, pagination)
|
||||
}
|
||||
|
||||
func (s *Service) ListTransactionsByAddress(
|
||||
ctx context.Context,
|
||||
address *types.Address,
|
||||
pagination *pagination.Pagination,
|
||||
) (*ListTransactonsOutput, error) {
|
||||
|
||||
return s.repo.ListTransactionsByAddress(ctx, address, pagination)
|
||||
}
|
||||
|
|
|
@ -175,7 +175,33 @@ func ExtractObservationHash(c *fiber.Ctx, l *zap.Logger) (string, error) {
|
|||
return hash, nil
|
||||
}
|
||||
|
||||
func ExtractAddress(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) {
|
||||
// ExtractAddressFromQueryParams parses the `address` parameter from the query string.
|
||||
//
|
||||
// If the parameter doesn't exist, the function returns a nil address without errors.
|
||||
func ExtractAddressFromQueryParams(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) {
|
||||
|
||||
val := c.Query("address")
|
||||
|
||||
if val == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Attempt to parse the address
|
||||
addr, err := types.StringToAddress(val, true /*acceptSolanaFormat*/)
|
||||
if err != nil {
|
||||
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
|
||||
l.Error("failed to decode address",
|
||||
zap.Error(err),
|
||||
zap.String("requestID", requestID),
|
||||
)
|
||||
return nil, response.NewInvalidParamError(c, "MALFORMED ADDR", errors.WithStack(err))
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// ExtractAddressFromPath parses the `id` parameter from the route path.
|
||||
func ExtractAddressFromPath(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) {
|
||||
|
||||
val := c.Params("id")
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ func NewController(srv *address.Service, logger *zap.Logger) *Controller {
|
|||
// @Router /api/v1/address/{address} [get]
|
||||
func (c *Controller) FindById(ctx *fiber.Ctx) error {
|
||||
|
||||
address, err := middleware.ExtractAddress(ctx, c.logger)
|
||||
address, err := middleware.ExtractAddressFromPath(ctx, c.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ func RegisterRoutes(
|
|||
// accounts resource
|
||||
api.Get("/address/:id", addressCtrl.FindById)
|
||||
|
||||
// analytics
|
||||
// analytics, transactions, custom endpoints
|
||||
api.Get("/global-tx/:chain/:emitter/:sequence", transactionCtrl.FindGlobalTransactionByID)
|
||||
api.Get("/last-txs", transactionCtrl.GetLastTransactions)
|
||||
api.Get("/scorecards", transactionCtrl.GetScorecards)
|
||||
|
@ -70,6 +70,7 @@ func RegisterRoutes(
|
|||
api.Get("/top-assets-by-volume", transactionCtrl.GetTopAssets)
|
||||
api.Get("/top-chain-pairs-by-num-transfers", transactionCtrl.GetTopChainPairs)
|
||||
api.Get("token/:chain/:token_address", transactionCtrl.GetTokenByChainAndAddress)
|
||||
api.Get("/transactions", transactionCtrl.ListTransactions)
|
||||
|
||||
// vaas resource
|
||||
vaas := api.Group("/vaas")
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/api/middleware"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -347,3 +348,81 @@ func (c *Controller) GetTokenByChainAndAddress(ctx *fiber.Ctx) error {
|
|||
|
||||
return ctx.JSON(token)
|
||||
}
|
||||
|
||||
// ListTransactions godoc
|
||||
// @Description Returns transactions. Output is paginated.
|
||||
// @Tags Wormscan
|
||||
// @ID list-transactions
|
||||
// @Param page query integer false "Page number. Starts at 0."
|
||||
// @Param pageSize query integer false "Number of elements per page."
|
||||
// @Success 200 {object} ListTransactionsResponse
|
||||
// @Failure 400
|
||||
// @Failure 500
|
||||
// @Router /api/v1/transactions/ [get]
|
||||
func (c *Controller) ListTransactions(ctx *fiber.Ctx) error {
|
||||
|
||||
// Extract query parameters
|
||||
pagination, err := middleware.ExtractPagination(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
address, err := middleware.ExtractAddressFromQueryParams(ctx, c.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Query transactions from the database
|
||||
var queryResult *transactions.ListTransactonsOutput
|
||||
if address != nil {
|
||||
queryResult, err = c.srv.ListTransactionsByAddress(ctx.Context(), address, pagination)
|
||||
} else {
|
||||
queryResult, err = c.srv.ListTransactions(ctx.Context(), pagination)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert query results into the response model
|
||||
response := ListTransactionsResponse{
|
||||
Transactions: make([]TransactionOverview, 0, len(queryResult.Transactions)),
|
||||
}
|
||||
for i := range queryResult.Transactions {
|
||||
|
||||
tx := TransactionOverview{
|
||||
ID: queryResult.Transactions[i].ID,
|
||||
OriginChain: queryResult.Transactions[i].EmitterChain,
|
||||
Timestamp: queryResult.Transactions[i].Timestamp,
|
||||
DestinationAddress: queryResult.Transactions[i].ToAddress,
|
||||
DestinationChain: queryResult.Transactions[i].ToChain,
|
||||
Symbol: queryResult.Transactions[i].Symbol,
|
||||
TokenAmount: queryResult.Transactions[i].TokenAmount,
|
||||
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
|
||||
|
||||
} else {
|
||||
tx.TxHash = queryResult.Transactions[i].TxHash
|
||||
}
|
||||
|
||||
// Set the status based on the outcome of the redeem transaction.
|
||||
if len(queryResult.Transactions[i].GlobalTransations) == 1 &&
|
||||
queryResult.Transactions[i].GlobalTransations[0].DestinationTx != nil &&
|
||||
queryResult.Transactions[i].GlobalTransations[0].DestinationTx.Status == domain.DstTxStatusConfirmed {
|
||||
|
||||
tx.Status = TxStatusCompleted
|
||||
} else {
|
||||
tx.Status = TxStatusOngoing
|
||||
}
|
||||
|
||||
response.Transactions = append(response.Transactions, tx)
|
||||
}
|
||||
|
||||
return ctx.JSON(response)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package transactions
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
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"`
|
||||
OriginChain sdk.ChainID `json:"originChain"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ListTransactionsResponse is the "200 OK" response model for `GET /api/v1/transactions`.
|
||||
type ListTransactionsResponse struct {
|
||||
Transactions []TransactionOverview `json:"transactions"`
|
||||
}
|
|
@ -24,3 +24,9 @@ const (
|
|||
// SourceTxStatusConfirmed indicates that the transaciton has been processed successfully.
|
||||
SourceTxStatusConfirmed SourceTxStatus = "confirmed"
|
||||
)
|
||||
|
||||
const (
|
||||
DstTxStatusFailedToProcess = "failed"
|
||||
DstTxStatusConfirmed = "completed"
|
||||
DstTxStatusUnkonwn = "unknown"
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/avast/retry-go"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/aptos"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -223,9 +224,9 @@ func (w *AptosWatcher) processTransaction(ctx context.Context, tx aptos.Transact
|
|||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
status := TxStatusFailedToProcess
|
||||
status := domain.DstTxStatusFailedToProcess
|
||||
if txResult.Success {
|
||||
status = TxStatusConfirmed
|
||||
status = domain.DstTxStatusConfirmed
|
||||
}
|
||||
updatedAt := time.Now()
|
||||
globalTx := storage.TransactionUpdate{
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/config"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/support"
|
||||
|
@ -53,11 +54,11 @@ type EvmTransaction struct {
|
|||
func getTxStatus(status string) string {
|
||||
switch status {
|
||||
case TxStatusSuccess:
|
||||
return TxStatusConfirmed
|
||||
return domain.DstTxStatusConfirmed
|
||||
case TxStatusFailReverted:
|
||||
return TxStatusFailedToProcess
|
||||
return domain.DstTxStatusFailedToProcess
|
||||
default:
|
||||
return TxStatusUnkonwn
|
||||
return domain.DstTxStatusUnkonwn
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
solana_types "github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/near/borsh-go"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/solana"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -378,7 +379,7 @@ func (w *SolanaWatcher) getAccountAddress(inst solana_types.CompiledInstruction,
|
|||
|
||||
func (w *SolanaWatcher) getStatus(txRpc *rpc.TransactionWithMeta) string {
|
||||
if txRpc.Meta != nil && txRpc.Meta.Err != nil {
|
||||
return TxStatusFailedToProcess
|
||||
return domain.DstTxStatusFailedToProcess
|
||||
}
|
||||
return TxStatusConfirmed
|
||||
return domain.DstTxStatusConfirmed
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/terra"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
@ -287,9 +288,9 @@ func filterTransactionMethod(method string) bool {
|
|||
|
||||
func getStatus(tx terra.Tx) string {
|
||||
if tx.Code == 0 {
|
||||
return TxStatusConfirmed
|
||||
return domain.DstTxStatusConfirmed
|
||||
}
|
||||
return TxStatusFailedToProcess
|
||||
return domain.DstTxStatusFailedToProcess
|
||||
}
|
||||
|
||||
func (w *TerraWatcher) Close() {
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package watcher
|
||||
|
||||
const (
|
||||
TxStatusFailedToProcess = "failed"
|
||||
TxStatusConfirmed = "completed"
|
||||
TxStatusUnkonwn = "unknown"
|
||||
)
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -38,27 +39,27 @@ func updateGlobalTransaction(ctx context.Context, tx storage.TransactionUpdate,
|
|||
// checkTxShouldBeUpdated checks if the transaction should be updated.
|
||||
func checkTxShouldBeUpdated(ctx context.Context, tx storage.TransactionUpdate, getGlobalTransactionByIDFunc FuncGetGlobalTransactionById) (bool, error) {
|
||||
switch tx.Destination.Status {
|
||||
case TxStatusConfirmed:
|
||||
case domain.DstTxStatusConfirmed:
|
||||
return true, nil
|
||||
case TxStatusFailedToProcess:
|
||||
case domain.DstTxStatusFailedToProcess:
|
||||
// check if the transaction exists from the same vaa ID.
|
||||
oldTx, err := getGlobalTransactionByIDFunc(ctx, tx.ID)
|
||||
if err != nil {
|
||||
return true, nil
|
||||
}
|
||||
// if the transaction was already confirmed, then no update it.
|
||||
if oldTx.Destination.Status == TxStatusConfirmed {
|
||||
if oldTx.Destination.Status == domain.DstTxStatusConfirmed {
|
||||
return false, ErrTxfailedCannotBeUpdated
|
||||
}
|
||||
return true, nil
|
||||
case TxStatusUnkonwn:
|
||||
case domain.DstTxStatusUnkonwn:
|
||||
// check if the transaction exists from the same vaa ID.
|
||||
oldTx, err := getGlobalTransactionByIDFunc(ctx, tx.ID)
|
||||
if err != nil {
|
||||
return true, nil
|
||||
}
|
||||
// if the transaction was already confirmed or failed to process, then no update it.
|
||||
if oldTx.Destination.Status == TxStatusConfirmed || oldTx.Destination.Status == TxStatusFailedToProcess {
|
||||
if oldTx.Destination.Status == domain.DstTxStatusConfirmed || oldTx.Destination.Status == domain.DstTxStatusFailedToProcess {
|
||||
return false, ErrTxUnknowCannotBeUpdated
|
||||
}
|
||||
return true, nil
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage"
|
||||
)
|
||||
|
||||
|
@ -21,7 +22,7 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status completed and does not exist transaction with the same vaa ID",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{}, storage.ErrDocNotFound
|
||||
|
@ -33,12 +34,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status completed and already exists a transaction with the same vaa ID with status completed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
@ -48,12 +49,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status completed and already exist a transaction with the same vaa ID with status failed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
@ -63,12 +64,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status completed and already exist a transaction with the same vaa ID with status unknown",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
@ -78,7 +79,7 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status failed and does not exist transaction with the same vaa ID",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{}, storage.ErrDocNotFound
|
||||
|
@ -90,12 +91,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status failed and already exists a transaction with the same vaa ID with status completed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: false,
|
||||
|
@ -105,12 +106,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status failed and already exist a transaction with the same vaa ID with status failed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
@ -120,12 +121,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status failed and already exist a transaction with the same vaa ID with status unknown",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
@ -135,7 +136,7 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status unknown and does not exist transaction with the same vaa ID",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{}, storage.ErrDocNotFound
|
||||
|
@ -147,12 +148,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status unknown and already exists a transaction with the same vaa ID with status completed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusConfirmed,
|
||||
Status: domain.DstTxStatusConfirmed,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: false,
|
||||
|
@ -162,12 +163,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status unknown and already exist a transaction with the same vaa ID with status failed",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusFailedToProcess,
|
||||
Status: domain.DstTxStatusFailedToProcess,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: false,
|
||||
|
@ -177,12 +178,12 @@ func TestCheckTxShouldBeUpdated(t *testing.T) {
|
|||
name: "tx with status unknown and already exist a transaction with the same vaa ID with status unknown",
|
||||
inputTx: storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}},
|
||||
inputGetGlobalTransactionByIDFunc: func(ctx context.Context, id string) (storage.TransactionUpdate, error) {
|
||||
return storage.TransactionUpdate{
|
||||
Destination: storage.DestinationTx{
|
||||
Status: TxStatusUnkonwn,
|
||||
Status: domain.DstTxStatusUnkonwn,
|
||||
}}, nil
|
||||
},
|
||||
expectedUpdate: true,
|
||||
|
|
Loading…
Reference in New Issue