2023-03-07 11:25:42 -08:00
|
|
|
package transactions
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
2023-03-22 09:38:43 -07:00
|
|
|
"github.com/shopspring/decimal"
|
2023-03-07 11:25:42 -08:00
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/api/middleware"
|
2023-05-10 13:39:18 -07:00
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
2023-03-07 11:25:42 -08:00
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Controller is the controller for the transactions resource.
|
|
|
|
type Controller struct {
|
|
|
|
srv *transactions.Service
|
|
|
|
logger *zap.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewController create a new controler.
|
|
|
|
func NewController(transactionsService *transactions.Service, logger *zap.Logger) *Controller {
|
|
|
|
return &Controller{
|
|
|
|
srv: transactionsService,
|
|
|
|
logger: logger.With(zap.String("module", "TransactionsController")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetLastTransactions godoc
|
|
|
|
// @Description Returns the number of transactions [vaa] by a defined time span and sample rate.
|
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID get-last-transactions
|
2023-05-03 10:01:55 -07:00
|
|
|
// @Param timeSpan query string false "Time Span, default: 1d, supported values: [1d, 1w, 1mo]"
|
|
|
|
// @Param sampleRate query string false "Sample Rate, default: 1h, supported values: [1h, 1d]"
|
2023-03-07 11:25:42 -08:00
|
|
|
// @Success 200 {object} []transactions.TransactionCountResult
|
|
|
|
// @Failure 400
|
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/last-txs [get]
|
|
|
|
func (c *Controller) GetLastTransactions(ctx *fiber.Ctx) error {
|
|
|
|
timeSpan, err := middleware.ExtractTimeSpan(ctx, c.logger)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sampleRate, err := middleware.ExtractSampleRate(ctx, c.logger)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
q := &transactions.TransactionCountQuery{
|
|
|
|
TimeSpan: timeSpan,
|
|
|
|
SampleRate: sampleRate,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get transaction count.
|
|
|
|
lastTrx, err := c.srv.GetTransactionCount(ctx.Context(), q)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(lastTrx)
|
|
|
|
}
|
|
|
|
|
2023-04-20 12:01:10 -07:00
|
|
|
// GetScorecards godoc
|
|
|
|
// @Description Returns a list of KPIs for Wormhole.
|
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID get-scorecards
|
|
|
|
// @Success 200 {object} ScorecardsResponse
|
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/scorecards [get]
|
|
|
|
func (c *Controller) GetScorecards(ctx *fiber.Ctx) error {
|
|
|
|
|
|
|
|
// Query indicators from the database
|
|
|
|
scorecards, err := c.srv.GetScorecards(ctx.Context())
|
|
|
|
if err != nil {
|
2023-05-04 16:17:03 -07:00
|
|
|
c.logger.Error("failed to get scorecards", zap.Error(err))
|
2023-04-20 12:01:10 -07:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert indicators to the response model
|
|
|
|
response := ScorecardsResponse{
|
|
|
|
TotalTxCount: scorecards.TotalTxCount,
|
2023-05-04 16:17:03 -07:00
|
|
|
TxCount24h: scorecards.TxCount24h,
|
|
|
|
Volume24h: scorecards.Volume24h,
|
2023-04-20 12:01:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(response)
|
|
|
|
}
|
|
|
|
|
2023-05-12 09:05:18 -07:00
|
|
|
// GetTopChainPairs godoc
|
|
|
|
// @Description Returns a list of the (emitter_chain, destination_chain) pairs with the highest number of transfers.
|
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID get-top-chain-pairs-by-num-transfers
|
|
|
|
// @Param timeSpan query string true "Time span, supported values: 7d, 15d, 30d."
|
|
|
|
// @Success 200 {object} TopChainPairsResponse
|
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/top-chain-pairs-by-num-transfers [get]
|
|
|
|
func (c *Controller) GetTopChainPairs(ctx *fiber.Ctx) error {
|
|
|
|
|
|
|
|
// Extract query parameters
|
|
|
|
timeSpan, err := middleware.ExtractTopStatisticsTimeSpan(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Query chain pairs from the database
|
|
|
|
chainPairDTOs, err := c.srv.GetTopChainPairs(ctx.Context(), timeSpan)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("failed to get top chain pairs by number of transfers", zap.Error(err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert DTOs to the response model
|
|
|
|
response := TopChainPairsResponse{
|
|
|
|
ChainPairs: make([]ChainPair, 0, len(chainPairDTOs)),
|
|
|
|
}
|
|
|
|
for i := range chainPairDTOs {
|
|
|
|
chainPair := ChainPair{
|
|
|
|
EmitterChain: chainPairDTOs[i].EmitterChain,
|
|
|
|
DestinationChain: chainPairDTOs[i].DestinationChain,
|
|
|
|
NumberOfTransfers: chainPairDTOs[i].NumberOfTransfers,
|
|
|
|
}
|
|
|
|
response.ChainPairs = append(response.ChainPairs, chainPair)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(response)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTopAssets godoc
|
|
|
|
// @Description Returns a list of the (emitter_chain, asset) pairs with the most volume.
|
2023-05-10 13:39:18 -07:00
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID get-top-assets-by-volume
|
2023-05-12 09:05:18 -07:00
|
|
|
// @Param timeSpan query string true "Time span, supported values: 7d, 15d, 30d."
|
|
|
|
// @Success 200 {object} TopAssetsResponse
|
2023-05-10 13:39:18 -07:00
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/top-assets-by-volume [get]
|
2023-05-12 09:05:18 -07:00
|
|
|
func (c *Controller) GetTopAssets(ctx *fiber.Ctx) error {
|
2023-05-10 13:39:18 -07:00
|
|
|
|
|
|
|
// Extract query parameters
|
2023-05-12 09:05:18 -07:00
|
|
|
timeSpan, err := middleware.ExtractTopStatisticsTimeSpan(ctx)
|
2023-05-10 13:39:18 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Query assets from the database
|
2023-05-12 09:05:18 -07:00
|
|
|
assetDTOs, err := c.srv.GetTopAssets(ctx.Context(), timeSpan)
|
2023-05-10 13:39:18 -07:00
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("failed to get top assets by volume", zap.Error(err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert DTOs to the response model
|
2023-05-12 09:05:18 -07:00
|
|
|
response := TopAssetsResponse{
|
2023-05-10 13:39:18 -07:00
|
|
|
Assets: make([]AssetWithVolume, 0, len(assetDTOs)),
|
|
|
|
}
|
|
|
|
for i := range assetDTOs {
|
|
|
|
|
|
|
|
// Look up the token symbol
|
|
|
|
tokenMeta, ok := domain.GetTokenByAddress(assetDTOs[i].TokenChain, assetDTOs[i].TokenAddress)
|
|
|
|
if !ok {
|
|
|
|
c.logger.Warn("failed to obtain token metadata in top volume chart",
|
|
|
|
zap.String("token_chain", assetDTOs[i].TokenChain.String()),
|
|
|
|
zap.String("token_address", assetDTOs[i].TokenAddress),
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate the response struct
|
|
|
|
asset := AssetWithVolume{
|
|
|
|
EmitterChain: assetDTOs[i].EmitterChain,
|
|
|
|
Volume: assetDTOs[i].Volume,
|
|
|
|
Symbol: tokenMeta.Symbol,
|
|
|
|
}
|
|
|
|
response.Assets = append(response.Assets, asset)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(response)
|
|
|
|
}
|
|
|
|
|
2023-03-07 11:25:42 -08:00
|
|
|
// GetChainActivity godoc
|
|
|
|
// @Description Returns a list of tx by source chain and destination chain.
|
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID x-chain-activity
|
|
|
|
// @Param start_time query string false "Star time (format: ISO-8601)."
|
|
|
|
// @Param end_time query string false "End time (format: ISO-8601)."
|
|
|
|
// @Param by query string false "Renders the results as notional or tx-count (default is notional)."
|
|
|
|
// @Param apps query string false "List of apps separated by comma (default is all apps)."
|
|
|
|
// @Success 200 {object} transactions.ChainActivity
|
|
|
|
// @Failure 400
|
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/x-chain-activity [get]
|
|
|
|
func (c *Controller) GetChainActivity(ctx *fiber.Ctx) error {
|
2023-04-04 06:49:27 -07:00
|
|
|
startTime, endTime, err := middleware.ExtractTimeRange(ctx)
|
2023-03-07 11:25:42 -08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
apps, err := middleware.ExtractApps(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
isNotional, err := middleware.ExtractIsNotional(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
q := &transactions.ChainActivityQuery{
|
|
|
|
Start: startTime,
|
|
|
|
End: endTime,
|
|
|
|
AppIDs: apps,
|
|
|
|
IsNotional: isNotional,
|
|
|
|
}
|
|
|
|
// Get the chain activity.
|
|
|
|
activity, err := c.srv.GetChainActivity(ctx.Context(), q)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("Error getting chain activity", zap.Error(err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert the result to the expected format.
|
2023-03-22 09:38:43 -07:00
|
|
|
txs, err := c.createChainActivityResponse(activity)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(ChainActivity{Txs: txs})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Controller) createChainActivityResponse(activity []transactions.ChainActivityResult) ([]Tx, error) {
|
2023-03-07 11:25:42 -08:00
|
|
|
txByChainID := make(map[int]*Tx)
|
2023-03-22 09:38:43 -07:00
|
|
|
total := decimal.Zero
|
2023-03-07 11:25:42 -08:00
|
|
|
for _, item := range activity {
|
|
|
|
chainSourceID, err := strconv.Atoi(item.ChainSourceID)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("Error during conversion of chainSourceId", zap.Error(err))
|
2023-03-22 09:38:43 -07:00
|
|
|
return nil, err
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
|
|
|
t, ok := txByChainID[chainSourceID]
|
|
|
|
if !ok {
|
|
|
|
destinations := make([]Destination, 0)
|
2023-03-22 09:38:43 -07:00
|
|
|
t = &Tx{Chain: chainSourceID, Volume: decimal.Zero, Percentage: 0, Destinations: destinations}
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
|
|
|
chainDestinationID, err := strconv.Atoi(item.ChainDestinationID)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("Error during conversion of chainDestinationId", zap.Error(err))
|
2023-03-22 09:38:43 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
volume, err := decimal.NewFromString(strconv.FormatUint(item.Volume, 10))
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("Error during conversion of volume to decimal", zap.Error(err))
|
|
|
|
return nil, err
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
2023-03-22 09:38:43 -07:00
|
|
|
destination := Destination{Chain: chainDestinationID, Volume: volume, Percentage: 0}
|
2023-03-07 11:25:42 -08:00
|
|
|
t.Destinations = append(t.Destinations, destination)
|
2023-03-22 09:38:43 -07:00
|
|
|
t.Volume = t.Volume.Add(volume)
|
2023-03-07 11:25:42 -08:00
|
|
|
txByChainID[chainSourceID] = t
|
2023-03-22 09:38:43 -07:00
|
|
|
total = total.Add(volume)
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
txs := make([]Tx, 0)
|
2023-03-22 09:38:43 -07:00
|
|
|
oneHundred := decimal.NewFromInt(100)
|
2023-03-07 11:25:42 -08:00
|
|
|
for _, item := range txByChainID {
|
2023-03-22 09:38:43 -07:00
|
|
|
if total.GreaterThan(decimal.Zero) {
|
|
|
|
percentage, _ := item.Volume.Div(total).Mul(oneHundred).Float64()
|
2023-03-07 11:25:42 -08:00
|
|
|
item.Percentage = percentage
|
|
|
|
}
|
|
|
|
for i, destination := range item.Destinations {
|
2023-03-22 09:38:43 -07:00
|
|
|
if item.Volume.GreaterThan(decimal.Zero) {
|
|
|
|
percentage, _ := destination.Volume.Div(item.Volume).Mul(oneHundred).Float64()
|
|
|
|
item.Destinations[i].Percentage = percentage
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
txs = append(txs, *item)
|
|
|
|
}
|
2023-03-22 09:38:43 -07:00
|
|
|
return txs, nil
|
2023-03-07 11:25:42 -08:00
|
|
|
}
|
2023-03-15 12:52:50 -07:00
|
|
|
|
|
|
|
// FindGlobalTransactionByID godoc
|
|
|
|
// @Description Find a global transaction by ID.
|
|
|
|
// @Tags Wormscan
|
|
|
|
// @ID find-global-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} Tx
|
|
|
|
// @Failure 400
|
|
|
|
// @Failure 500
|
|
|
|
// @Router /api/v1/global-tx/{chain_id}/{emitter}/{seq} [get]
|
|
|
|
func (c *Controller) FindGlobalTransactionByID(ctx *fiber.Ctx) error {
|
|
|
|
chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
globalTransaction, err := c.srv.FindGlobalTransactionByID(ctx.Context(), chainID, emitter, strconv.FormatUint(seq, 10))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx.JSON(globalTransaction)
|
|
|
|
}
|