Add route `GET /api/v1/address/{address}` (#228)
### Summary This pull request adds the new route `GET /api/v1/address/{address}`, which returns the transactions in which the given address participated. Examples: * https://api.staging.wormscan.io/api/v1/address/0x0000000000000000000000001ef2e0219841d1a540d99c432a6eddb75deed1b7 *0000000000
40d99c432a6eddb75deed1b7 *0000000000
40d99c432a6eddb75deed1b7?page=0&pageSize=2 *0000000000
40d99c432a6eddb75deed1b7?page=1&pageSize=2 * https://api.staging.wormscan.io/api/v1/address/1111111111114Sd894pYPPeXjZCDN5Gv8KCzwFGN Tracking issue: https://github.com/wormhole-foundation/wormhole-explorer/issues/222
This commit is contained in:
parent
24673d4f31
commit
6fcf8f8270
|
@ -1,5 +1,4 @@
|
||||||
// Package docs GENERATED BY SWAG; DO NOT EDIT
|
// Code generated by swaggo/swag. DO NOT EDIT
|
||||||
// This file was generated by swaggo/swag
|
|
||||||
package docs
|
package docs
|
||||||
|
|
||||||
import "github.com/swaggo/swag"
|
import "github.com/swaggo/swag"
|
||||||
|
@ -25,6 +24,53 @@ const docTemplate = `{
|
||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/address/{address}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lookup an address",
|
||||||
|
"tags": [
|
||||||
|
"Wormscan"
|
||||||
|
],
|
||||||
|
"operationId": "find-address-by-id",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "address",
|
||||||
|
"name": "address",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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/response.Response-address_AddressOverview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/global-tx/{chain_id}/{emitter}/{seq}": {
|
"/api/v1/global-tx/{chain_id}/{emitter}/{seq}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Find a global transaction by ID.",
|
"description": "Find a global transaction by ID.",
|
||||||
|
@ -1515,6 +1561,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"address.AddressOverview": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vaas": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/vaa.VaaDoc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": {
|
"github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2046,6 +2103,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"response.Response-address_AddressOverview": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/address.AddressOverview"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"$ref": "#/definitions/response.ResponsePagination"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"response.Response-array_governor_EnqueuedVaaDetail": {
|
"response.Response-array_governor_EnqueuedVaaDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2356,6 +2424,10 @@ const docTemplate = `{
|
||||||
"indexedAt": {
|
"indexedAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"nativeTxHash": {
|
||||||
|
"description": "NativeTxHash is an extension field - it is not present in the guardian API.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"payload": {
|
"payload": {
|
||||||
"description": "Payload is an extension field - it is not present in the guardian API.",
|
"description": "Payload is an extension field - it is not present in the guardian API.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
@ -17,6 +17,53 @@
|
||||||
},
|
},
|
||||||
"basePath": "/v1",
|
"basePath": "/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/address/{address}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lookup an address",
|
||||||
|
"tags": [
|
||||||
|
"Wormscan"
|
||||||
|
],
|
||||||
|
"operationId": "find-address-by-id",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "address",
|
||||||
|
"name": "address",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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/response.Response-address_AddressOverview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/global-tx/{chain_id}/{emitter}/{seq}": {
|
"/api/v1/global-tx/{chain_id}/{emitter}/{seq}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Find a global transaction by ID.",
|
"description": "Find a global transaction by ID.",
|
||||||
|
@ -1507,6 +1554,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"address.AddressOverview": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vaas": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/vaa.VaaDoc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": {
|
"github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2038,6 +2096,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"response.Response-address_AddressOverview": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/address.AddressOverview"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"$ref": "#/definitions/response.ResponsePagination"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"response.Response-array_governor_EnqueuedVaaDetail": {
|
"response.Response-array_governor_EnqueuedVaaDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2348,6 +2417,10 @@
|
||||||
"indexedAt": {
|
"indexedAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"nativeTxHash": {
|
||||||
|
"description": "NativeTxHash is an extension field - it is not present in the guardian API.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"payload": {
|
"payload": {
|
||||||
"description": "Payload is an extension field - it is not present in the guardian API.",
|
"description": "Payload is an extension field - it is not present in the guardian API.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
basePath: /v1
|
basePath: /v1
|
||||||
definitions:
|
definitions:
|
||||||
|
address.AddressOverview:
|
||||||
|
properties:
|
||||||
|
vaas:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/vaa.VaaDoc'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet:
|
github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet:
|
||||||
properties:
|
properties:
|
||||||
addresses:
|
addresses:
|
||||||
|
@ -345,6 +352,13 @@ definitions:
|
||||||
version:
|
version:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
response.Response-address_AddressOverview:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/address.AddressOverview'
|
||||||
|
pagination:
|
||||||
|
$ref: '#/definitions/response.ResponsePagination'
|
||||||
|
type: object
|
||||||
response.Response-array_governor_EnqueuedVaaDetail:
|
response.Response-array_governor_EnqueuedVaaDetail:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
|
@ -566,6 +580,10 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
indexedAt:
|
indexedAt:
|
||||||
type: string
|
type: string
|
||||||
|
nativeTxHash:
|
||||||
|
description: NativeTxHash is an extension field - it is not present in the
|
||||||
|
guardian API.
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
description: Payload is an extension field - it is not present in the guardian
|
description: Payload is an extension field - it is not present in the guardian
|
||||||
|
@ -609,6 +627,37 @@ info:
|
||||||
title: Wormhole Guardian API
|
title: Wormhole Guardian API
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
|
/api/v1/address/{address}:
|
||||||
|
get:
|
||||||
|
description: Lookup an address
|
||||||
|
operationId: find-address-by-id
|
||||||
|
parameters:
|
||||||
|
- description: address
|
||||||
|
in: path
|
||||||
|
name: address
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- 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/response.Response-address_AddressOverview'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
tags:
|
||||||
|
- Wormscan
|
||||||
/api/v1/global-tx/{chain_id}/{emitter}/{seq}:
|
/api/v1/global-tx/{chain_id}/{emitter}/{seq}:
|
||||||
get:
|
get:
|
||||||
description: Find a global transaction by ID.
|
description: Find a global transaction by ID.
|
||||||
|
|
|
@ -85,7 +85,7 @@ require (
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||||
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0
|
||||||
github.com/multiformats/go-base32 v0.0.4 // indirect
|
github.com/multiformats/go-base32 v0.0.4 // indirect
|
||||||
github.com/multiformats/go-base36 v0.1.0 // indirect
|
github.com/multiformats/go-base36 v0.1.0 // indirect
|
||||||
github.com/multiformats/go-multiaddr v0.6.0 // indirect
|
github.com/multiformats/go-multiaddr v0.6.0 // indirect
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
||||||
|
|
||||||
|
type AddressOverview struct {
|
||||||
|
Vaas []*vaa.VaaDoc `json:"vaas"`
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/types"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
db *mongo.Database
|
||||||
|
logger *zap.Logger
|
||||||
|
|
||||||
|
collections struct {
|
||||||
|
parsedVaa *mongo.Collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(db *mongo.Database, logger *zap.Logger) *Repository {
|
||||||
|
return &Repository{db: db,
|
||||||
|
logger: logger.With(zap.String("module", "AddressRepository")),
|
||||||
|
collections: struct {
|
||||||
|
parsedVaa *mongo.Collection
|
||||||
|
}{
|
||||||
|
parsedVaa: db.Collection("parsedVaa"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAddressOverviewParams struct {
|
||||||
|
Address *types.Address
|
||||||
|
Skip int64
|
||||||
|
Limit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetAddressOverview(ctx context.Context, params *GetAddressOverviewParams) (*AddressOverview, error) {
|
||||||
|
|
||||||
|
// build a query pipeline based on input parameters
|
||||||
|
var pipeline mongo.Pipeline
|
||||||
|
{
|
||||||
|
// filter by address
|
||||||
|
pipeline = append(pipeline, bson.D{
|
||||||
|
{"$match", bson.D{
|
||||||
|
{"$or", bson.A{
|
||||||
|
bson.D{{"result.fromAddress", bson.D{{"$eq", "0x" + params.Address.Hex()}}}},
|
||||||
|
bson.D{{"result.toAddress", bson.M{"$eq": "0x" + params.Address.Hex()}}},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// specify sorting criteria
|
||||||
|
pipeline = append(pipeline, bson.D{
|
||||||
|
{"$sort", bson.D{bson.E{"indexedAt", -1}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// left outer join on the `vaas` collection
|
||||||
|
pipeline = append(pipeline, bson.D{
|
||||||
|
{"$lookup", bson.D{
|
||||||
|
{"from", "vaas"},
|
||||||
|
{"localField", "_id"},
|
||||||
|
{"foreignField", "_id"},
|
||||||
|
{"as", "vaas"},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// skip initial results
|
||||||
|
if params.Skip != 0 {
|
||||||
|
pipeline = append(pipeline, bson.D{
|
||||||
|
{"$skip", params.Skip},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit size of results
|
||||||
|
pipeline = append(pipeline, bson.D{
|
||||||
|
{"$limit", params.Limit},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute the aggregation pipeline
|
||||||
|
cur, err := r.collections.parsedVaa.Aggregate(ctx, pipeline)
|
||||||
|
if err != nil {
|
||||||
|
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
||||||
|
r.logger.Error("failed execute Aggregate command to get vaa with payload",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("params", params),
|
||||||
|
zap.String("requestID", requestID),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read results from cursor
|
||||||
|
var documents []struct {
|
||||||
|
ID string `bson:"_id"`
|
||||||
|
Vaas []vaa.VaaDoc `bson:"vaas"`
|
||||||
|
}
|
||||||
|
err = cur.All(ctx, &documents)
|
||||||
|
if err != nil {
|
||||||
|
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
||||||
|
r.logger.Error("failed to decode cursor for account activity",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("params", params),
|
||||||
|
zap.String("requestID", requestID),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the result and return
|
||||||
|
var vaas []*vaa.VaaDoc
|
||||||
|
for i := range documents {
|
||||||
|
if len(documents[i].Vaas) != 1 {
|
||||||
|
r.logger.Warn("expected exactly 1 vaa document",
|
||||||
|
zap.Int("numVaas", len(documents[i].Vaas)),
|
||||||
|
zap.String("_id", documents[i].ID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
vaas = append(vaas, &documents[i].Vaas[0])
|
||||||
|
}
|
||||||
|
return &AddressOverview{Vaas: vaas}, nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/response"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/types"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo *Repository
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(r *Repository, logger *zap.Logger) *Service {
|
||||||
|
|
||||||
|
srv := Service{
|
||||||
|
repo: r,
|
||||||
|
logger: logger.With(zap.String("module", "AddressService")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAddressOverview(
|
||||||
|
ctx context.Context,
|
||||||
|
address *types.Address,
|
||||||
|
pagination *pagination.Pagination,
|
||||||
|
) (*response.Response[*AddressOverview], error) {
|
||||||
|
|
||||||
|
response := &response.Response[*AddressOverview]{}
|
||||||
|
|
||||||
|
p := GetAddressOverviewParams{
|
||||||
|
Address: address,
|
||||||
|
Skip: pagination.Skip,
|
||||||
|
Limit: pagination.Limit,
|
||||||
|
}
|
||||||
|
overview, err := s.repo.GetAddressOverview(ctx, &p)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Data = overview
|
||||||
|
return response, nil
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||||
|
|
||||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/address"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure"
|
||||||
|
@ -102,6 +103,7 @@ func main() {
|
||||||
influxCli := newInfluxClient(cfg.Influx.URL, cfg.Influx.Token)
|
influxCli := newInfluxClient(cfg.Influx.URL, cfg.Influx.Token)
|
||||||
|
|
||||||
// Set up repositories
|
// Set up repositories
|
||||||
|
addressRepo := address.NewRepository(db, rootLogger)
|
||||||
vaaRepo := vaa.NewRepository(db, rootLogger)
|
vaaRepo := vaa.NewRepository(db, rootLogger)
|
||||||
obsRepo := observations.NewRepository(db, rootLogger)
|
obsRepo := observations.NewRepository(db, rootLogger)
|
||||||
governorRepo := governor.NewRepository(db, rootLogger)
|
governorRepo := governor.NewRepository(db, rootLogger)
|
||||||
|
@ -110,6 +112,7 @@ func main() {
|
||||||
transactionsRepo := transactions.NewRepository(influxCli, cfg.Influx.Organization, cfg.Influx.Bucket, db, rootLogger)
|
transactionsRepo := transactions.NewRepository(influxCli, cfg.Influx.Organization, cfg.Influx.Bucket, db, rootLogger)
|
||||||
|
|
||||||
// Set up services
|
// Set up services
|
||||||
|
addressService := address.NewService(addressRepo, rootLogger)
|
||||||
vaaService := vaa.NewService(vaaRepo, cacheGetFunc, rootLogger)
|
vaaService := vaa.NewService(vaaRepo, cacheGetFunc, rootLogger)
|
||||||
obsService := observations.NewService(obsRepo, rootLogger)
|
obsService := observations.NewService(obsRepo, rootLogger)
|
||||||
governorService := governor.NewService(governorRepo, rootLogger)
|
governorService := governor.NewService(governorRepo, rootLogger)
|
||||||
|
@ -136,7 +139,7 @@ func main() {
|
||||||
|
|
||||||
// Set up route handlers
|
// Set up route handlers
|
||||||
app.Get("/swagger.json", GetSwagger)
|
app.Get("/swagger.json", GetSwagger)
|
||||||
wormscan.RegisterRoutes(app, rootLogger, vaaService, obsService, governorService, infrastructureService, transactionsService)
|
wormscan.RegisterRoutes(app, rootLogger, addressService, vaaService, obsService, governorService, infrastructureService, transactionsService)
|
||||||
guardian.RegisterRoutes(cfg, app, rootLogger, vaaService, governorService, heartbeatsService)
|
guardian.RegisterRoutes(cfg, app, rootLogger, vaaService, governorService, heartbeatsService)
|
||||||
|
|
||||||
// Set up gRPC handlers
|
// Set up gRPC handlers
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
solana "github.com/gagliardetto/solana-go"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/response"
|
"github.com/wormhole-foundation/wormhole-explorer/api/response"
|
||||||
|
@ -45,27 +44,14 @@ func ExtractEmitterAddr(c *fiber.Ctx, l *zap.Logger, chainIdHint *sdk.ChainID) (
|
||||||
|
|
||||||
emitterStr := c.Params("emitter")
|
emitterStr := c.Params("emitter")
|
||||||
|
|
||||||
// If the chain ID is Solana, attempt to parse the emitter as a Solana address.
|
// Decide whether to accept the Solana address format based on the context
|
||||||
|
var acceptSolanaFormat bool
|
||||||
if chainIdHint != nil && *chainIdHint == sdk.ChainIDSolana {
|
if chainIdHint != nil && *chainIdHint == sdk.ChainIDSolana {
|
||||||
|
acceptSolanaFormat = true
|
||||||
// If the address fails to parse, just fall back to the Wormhole format.
|
|
||||||
sig, err := solana.PublicKeyFromBase58(emitterStr)
|
|
||||||
if err == nil {
|
|
||||||
// This step is not expected to fail, since Solana and Wormhole addresses have the same size.
|
|
||||||
// However, if it does, we log the error.
|
|
||||||
emitter, err := types.BytesToAddress(sig[:])
|
|
||||||
if err == nil {
|
|
||||||
return emitter, nil
|
|
||||||
}
|
|
||||||
l.Warn("failed to convert Solana address to Wormhole address",
|
|
||||||
zap.String("emitterAddress", emitterStr),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to parse the address according to the Wormhole hex format.
|
// Attempt to parse the address
|
||||||
emitter, err := types.StringToAddress(emitterStr)
|
emitter, err := types.StringToAddress(emitterStr, acceptSolanaFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
|
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
|
||||||
l.Error("failed to convert emitter to wormhole address",
|
l.Error("failed to convert emitter to wormhole address",
|
||||||
|
@ -107,8 +93,8 @@ func ExtractGuardianAddress(c *fiber.Ctx, l *zap.Logger) (*types.Address, error)
|
||||||
return nil, response.NewInvalidParamError(c, "MALFORMED GUARDIAN ADDR", nil)
|
return nil, response.NewInvalidParamError(c, "MALFORMED GUARDIAN ADDR", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the address
|
// Attempt to parse the address
|
||||||
guardianAddress, err := types.StringToAddress(tmp)
|
guardianAddress, err := types.StringToAddress(tmp, false /*acceptSolanaFormat*/)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
|
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
|
||||||
l.Error("failed to decode guardian address",
|
l.Error("failed to decode guardian address",
|
||||||
|
@ -188,6 +174,24 @@ func ExtractObservationHash(c *fiber.Ctx, l *zap.Logger) (string, error) {
|
||||||
return hash, nil
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractAddress(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) {
|
||||||
|
|
||||||
|
val := c.Params("id")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// GetTxHash parses the `txHash` parameter from query params.
|
// GetTxHash parses the `txHash` parameter from query params.
|
||||||
func GetTxHash(c *fiber.Ctx, l *zap.Logger) (*types.TxHash, error) {
|
func GetTxHash(c *fiber.Ctx, l *zap.Logger) (*types.TxHash, error) {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/address"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/middleware" // required by swaggo
|
||||||
|
_ "github.com/wormhole-foundation/wormhole-explorer/api/response" // required by swaggo
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
srv *address.Service
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(srv *address.Service, logger *zap.Logger) *Controller {
|
||||||
|
|
||||||
|
c := Controller{
|
||||||
|
srv: srv,
|
||||||
|
logger: logger.With(zap.String("module", "AddressController")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindById godoc
|
||||||
|
// @Description Lookup an address
|
||||||
|
// @Tags Wormscan
|
||||||
|
// @ID find-address-by-id
|
||||||
|
// @Param address path string true "address"
|
||||||
|
// @Param page query integer false "Page number. Starts at 0."
|
||||||
|
// @Param pageSize query integer false "Number of elements per page."
|
||||||
|
// @Success 200 {object} response.Response[address.AddressOverview]
|
||||||
|
// @Failure 400
|
||||||
|
// @Failure 404
|
||||||
|
// @Failure 500
|
||||||
|
// @Router /api/v1/address/{address} [get]
|
||||||
|
func (c *Controller) FindById(ctx *fiber.Ctx) error {
|
||||||
|
|
||||||
|
address, err := middleware.ExtractAddress(ctx, c.logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination, err := middleware.ExtractPagination(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.srv.GetAddressOverview(ctx.Context(), address, pagination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(response.Data.Vaas) == 0 {
|
||||||
|
return errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(response)
|
||||||
|
}
|
|
@ -6,11 +6,13 @@ import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
addrsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/address"
|
||||||
govsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor"
|
govsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor"
|
||||||
infrasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure"
|
infrasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure"
|
||||||
obssvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations"
|
obssvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations"
|
||||||
trxsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
trxsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
||||||
vaasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
vaasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/address"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/governor"
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/governor"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/infrastructure"
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/infrastructure"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/observations"
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/observations"
|
||||||
|
@ -32,6 +34,7 @@ var cacheConfig = cache.Config{
|
||||||
func RegisterRoutes(
|
func RegisterRoutes(
|
||||||
app *fiber.App,
|
app *fiber.App,
|
||||||
rootLogger *zap.Logger,
|
rootLogger *zap.Logger,
|
||||||
|
addressService *addrsvc.Service,
|
||||||
vaaService *vaasvc.Service,
|
vaaService *vaasvc.Service,
|
||||||
obsService *obssvc.Service,
|
obsService *obssvc.Service,
|
||||||
governorService *govsvc.Service,
|
governorService *govsvc.Service,
|
||||||
|
@ -40,6 +43,7 @@ func RegisterRoutes(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Set up controllers
|
// Set up controllers
|
||||||
|
addressCtrl := address.NewController(addressService, rootLogger)
|
||||||
vaaCtrl := vaa.NewController(vaaService, rootLogger)
|
vaaCtrl := vaa.NewController(vaaService, rootLogger)
|
||||||
observationsCtrl := observations.NewController(obsService, rootLogger)
|
observationsCtrl := observations.NewController(obsService, rootLogger)
|
||||||
governorCtrl := governor.NewController(governorService, rootLogger)
|
governorCtrl := governor.NewController(governorService, rootLogger)
|
||||||
|
@ -55,6 +59,9 @@ func RegisterRoutes(
|
||||||
api.Get("/ready", infrastructureCtrl.ReadyCheck)
|
api.Get("/ready", infrastructureCtrl.ReadyCheck)
|
||||||
api.Get("/version", infrastructureCtrl.Version)
|
api.Get("/version", infrastructureCtrl.Version)
|
||||||
|
|
||||||
|
// accounts resource
|
||||||
|
api.Get("/address/:id", addressCtrl.FindById)
|
||||||
|
|
||||||
// analytics
|
// analytics
|
||||||
api.Get("/last-txs", transactionCtrl.GetLastTransactions)
|
api.Get("/last-txs", transactionCtrl.GetLastTransactions)
|
||||||
api.Get("/x-chain-activity", transactionCtrl.GetChainActivity)
|
api.Get("/x-chain-activity", transactionCtrl.GetChainActivity)
|
||||||
|
|
|
@ -221,18 +221,23 @@ func (h *Handler) GovernorGetEnqueuedVAAs(ctx context.Context, _ *publicrpcv1.Go
|
||||||
|
|
||||||
// GovernorIsVAAEnqueued check if a vaa is enqueued.
|
// GovernorIsVAAEnqueued check if a vaa is enqueued.
|
||||||
func (h *Handler) GovernorIsVAAEnqueued(ctx context.Context, request *publicrpcv1.GovernorIsVAAEnqueuedRequest) (*publicrpcv1.GovernorIsVAAEnqueuedResponse, error) {
|
func (h *Handler) GovernorIsVAAEnqueued(ctx context.Context, request *publicrpcv1.GovernorIsVAAEnqueuedRequest) (*publicrpcv1.GovernorIsVAAEnqueuedResponse, error) {
|
||||||
|
|
||||||
if request.MessageId == nil {
|
if request.MessageId == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "Parameters are required")
|
return nil, status.Error(codes.InvalidArgument, "Parameters are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
chainID := vaa.ChainID(request.MessageId.EmitterChain)
|
chainID := vaa.ChainID(request.MessageId.EmitterChain)
|
||||||
emitterAddress, err := types.StringToAddress(request.MessageId.EmitterAddress)
|
|
||||||
|
emitterAddress, err := types.StringToAddress(request.MessageId.EmitterAddress, false /*acceptSolanaFormat*/)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "Invalid emitter address")
|
return nil, status.Error(codes.InvalidArgument, "Invalid emitter address")
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnqueued, err := h.govSrv.IsVaaEnqueued(ctx, chainID, emitterAddress, strconv.FormatUint(request.MessageId.Sequence, 10))
|
isEnqueued, err := h.govSrv.IsVaaEnqueued(ctx, chainID, emitterAddress, strconv.FormatUint(request.MessageId.Sequence, 10))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &publicrpcv1.GovernorIsVAAEnqueuedResponse{IsEnqueued: isEnqueued}, nil
|
return &publicrpcv1.GovernorIsVAAEnqueuedResponse{IsEnqueued: isEnqueued}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,8 +26,23 @@ func BytesToAddress(b []byte) (*Address, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringToAddress converts a hex-encoded address string into an *Address.
|
// StringToAddress converts a hex-encoded address string into an *Address.
|
||||||
func StringToAddress(s string) (*Address, error) {
|
func StringToAddress(s string, acceptSolanaFormat bool) (*Address, error) {
|
||||||
|
|
||||||
|
// Attempt to parse a Solana address (i.e.: 32 bytes encoded as base58).
|
||||||
|
// If it fails, fall back to parsing a regular Wormhole address.
|
||||||
|
if acceptSolanaFormat {
|
||||||
|
|
||||||
|
sig, err := solana.PublicKeyFromBase58(s)
|
||||||
|
if err == nil {
|
||||||
|
// This step is not expected to fail, since Solana and Wormhole addresses have the same size.
|
||||||
|
emitter, err := BytesToAddress(sig[:])
|
||||||
|
if err == nil {
|
||||||
|
return emitter, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse a regular Wormhole address (i.e.: 32 bytes encoded as hex).
|
||||||
a, err := vaa.StringToAddress(s)
|
a, err := vaa.StringToAddress(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -7,6 +7,7 @@ func Test_Address_ShortString(t *testing.T) {
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Input string
|
Input string
|
||||||
|
AcceptSolanaFormat bool
|
||||||
Hex string
|
Hex string
|
||||||
ShortHex string
|
ShortHex string
|
||||||
}{
|
}{
|
||||||
|
@ -40,12 +41,31 @@ func Test_Address_ShortString(t *testing.T) {
|
||||||
Hex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5",
|
Hex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5",
|
||||||
ShortHex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5",
|
ShortHex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Input: "31Sof5r1xi7dfcaz4x9Kuwm8J9ueAdDduMcme59sP8gc",
|
||||||
|
AcceptSolanaFormat: true,
|
||||||
|
Hex: "1dd48d0ee1fe7059b2866507b84f5f4259d7408c812e88bd6260a4914f7a2605",
|
||||||
|
ShortHex: "1dd48d0ee1fe7059b2866507b84f5f4259d7408c812e88bd6260a4914f7a2605",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "31Sof5r1xi7dfcaz4x9Kuwm8J9ueAdDduMcme59sP8gc",
|
||||||
|
AcceptSolanaFormat: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range testCases {
|
for i := range testCases {
|
||||||
tc := &testCases[i]
|
tc := &testCases[i]
|
||||||
|
|
||||||
addr, err := StringToAddress(tc.Input)
|
addr, err := StringToAddress(tc.Input, tc.AcceptSolanaFormat /*acceptSolanaFormat*/)
|
||||||
|
|
||||||
|
if tc.Hex == "" {
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected error, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to parse address %s: %v", tc.Input, err)
|
t.Fatalf("failed to parse address %s: %v", tc.Input, err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue