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
2023-06-01 12:46:24 -07:00
// @Description Returns the number of transactions by a defined time span and sample rate.
2023-03-07 11:25:42 -08:00
// @Tags Wormscan
// @ID get-last-transactions
2023-05-15 12:59:54 -07:00
// @Param timeSpan query string false "Time Span, default: 1d, supported values: [1d, 1w, 1mo]. 1mo is 30 days."
// @Param sampleRate query string false "Sample Rate, default: 1h, supported values: [1h, 1d]. Valid configurations with timeSpan: 1d/1h, 1w/1d, 1mo/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 {
2023-05-15 12:59:54 -07:00
timeSpan , sampleRate , err := middleware . ExtractTimeSpanAndSampleRate ( ctx , c . logger )
2023-03-07 11:25:42 -08:00
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.
2023-06-01 12:46:24 -07:00
// @Description TVL is total value locked by token bridge contracts in USD.
// @Description Volume is the all-time total volume transferred through the token bridge in USD.
// @Description 24h volume is the volume transferred through the token bridge in the last 24 hours, in USD.
// @Description Total Tx count is the number of transaction bridging assets since the creation of the network (does not include Pyth or other messages).
// @Description 24h tx count is the number of transaction bridging assets in the last 24 hours (does not include Pyth or other messages).
// @Description Total messages is the number of VAAs emitted since the creation of the network (includes Pyth messages).
2023-04-20 12:01:10 -07:00
// @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 {
2023-05-18 07:14:36 -07:00
Messages24h : scorecards . Messages24h ,
2023-04-20 12:01:10 -07:00
TotalTxCount : scorecards . TotalTxCount ,
2023-05-15 11:15:12 -07:00
TotalVolume : scorecards . TotalTxVolume ,
2023-05-18 07:14:36 -07:00
Tvl : scorecards . Tvl ,
2023-05-23 07:40:54 -07:00
TxCount24h : scorecards . TxCount24h ,
2023-05-04 16:17:03 -07:00
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
2023-06-01 12:46:24 -07:00
// @Description Returns a list of the emitter_chain and destination_chain pair ordered by transfer count.
2023-05-12 09:05:18 -07:00
// @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
2023-06-01 12:46:24 -07:00
// @Description Returns a list of emitter_chain and asset pairs with ordered by volume.
// @Description The volume is calculated using the notional price of the symbol at the day the VAA was emitted.
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 {
asset := AssetWithVolume {
EmitterChain : assetDTOs [ i ] . EmitterChain ,
2023-05-15 13:30:15 -07:00
TokenChain : assetDTOs [ i ] . TokenChain ,
TokenAddress : assetDTOs [ i ] . TokenAddress ,
2023-05-10 13:39:18 -07:00
Volume : assetDTOs [ i ] . Volume ,
}
2023-05-15 13:30:15 -07:00
// Look up the token symbol
tokenMeta , ok := domain . GetTokenByAddress ( assetDTOs [ i ] . TokenChain , assetDTOs [ i ] . TokenAddress )
if ok {
2023-05-30 07:14:19 -07:00
asset . Symbol = tokenMeta . Symbol . String ( )
2023-05-15 13:30:15 -07:00
}
2023-05-10 13:39:18 -07:00
response . Assets = append ( response . Assets , asset )
}
return ctx . JSON ( response )
}
2023-03-07 11:25:42 -08:00
// GetChainActivity godoc
2023-06-01 12:46:24 -07:00
// @Description Returns a list of chain pairs by origin chain and destination chain.
// @Description The list could be rendered by volume or transaction count.
// @Description The volume is calculated using the notional price of the symbol at the day the VAA was emitted.
2023-03-07 11:25:42 -08:00
// @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)."
2023-06-01 12:46:24 -07:00
// @Param by query string false "Renders the results using volume or tx count (default is volume)."
2023-03-07 11:25:42 -08:00
// @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-05-23 07:40:54 -07:00
txs , err := c . createChainActivityResponse ( activity , isNotional )
2023-03-22 09:38:43 -07:00
if err != nil {
return err
}
return ctx . JSON ( ChainActivity { Txs : txs } )
}
2023-05-23 07:40:54 -07:00
func ( c * Controller ) createChainActivityResponse ( activity [ ] transactions . ChainActivityResult , isNotional bool ) ( [ ] 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
}
2023-05-23 07:40:54 -07:00
if isNotional {
item . Volume = convertToDecimal ( item . Volume )
}
2023-03-07 11:25:42 -08:00
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
}
2023-05-23 07:40:54 -07:00
if isNotional {
item . Destinations [ i ] . Volume = convertToDecimal ( destination . Volume )
}
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
2023-06-01 12:46:24 -07:00
// @Description Find a global transaction by VAA ID
// @Description Global transactions is a logical association of two transactions that are related to each other by a unique VAA ID.
// @Description The first transaction is created on the origin chain when the VAA is emitted.
// @Description The second transaction is created on the destination chain when the VAA is redeemed.
// @Description If the response only contains an origin tx the VAA was not redeemed.
2023-03-15 12:52:50 -07:00
// @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 )
}
2023-05-23 07:40:54 -07:00
func convertToDecimal ( amount decimal . Decimal ) decimal . Decimal {
eigthDecimals := decimal . NewFromInt ( 1_0000_0000 )
return amount . Div ( eigthDecimals )
}
2023-05-31 06:29:16 -07:00
// GetTokenByChainAndAddress godoc
// @Description Returns a token symbol, coingecko id and address by chain and token address.
// @Tags Wormscan
// @ID get-token-by-chain-and-address
// @Param chain_id path integer true "id of the blockchain"
// @Param token_address path string true "token address"
// @Success 200 {object} Token
// @Failure 400
// @Failure 404
// @Router /api/v1/token/{chain}/{token_address} [get]
func ( c * Controller ) GetTokenByChainAndAddress ( ctx * fiber . Ctx ) error {
chain , err := middleware . ExtractChainID ( ctx , c . logger )
if err != nil {
return err
}
tokenAddress , err := middleware . ExtractTokenAddress ( ctx , c . logger )
if err != nil {
return err
}
token , err := c . srv . GetTokenByChainAndAddress ( ctx . Context ( ) , chain , tokenAddress )
if err != nil {
return err
}
return ctx . JSON ( token )
}