Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ func RunApi(cmd *cobra.Command, args []string) {
// token balance queries
root.GET("/balances/:owner/:type", handlers.GetTokenBalancesByType)

root.GET("/balances/:owner", handlers.GetTokenBalancesByType)

// token holder queries
root.GET("/holders/:address", handlers.GetTokenHoldersByType)

Expand Down
49 changes: 48 additions & 1 deletion internal/handlers/token_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"fmt"
"math/big"
"strings"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -48,7 +49,11 @@ func GetTokenBalancesByType(c *gin.Context) {
return
}
tokenType := c.Param("type")
if tokenType != "erc20" && tokenType != "erc1155" && tokenType != "erc721" {
if tokenType == "" {
tokenType = c.Query("token_type")
}

if tokenType != "" && tokenType != "erc20" && tokenType != "erc1155" && tokenType != "erc721" {
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token type '%s'", tokenType))
return
}
Expand All @@ -62,6 +67,17 @@ func GetTokenBalancesByType(c *gin.Context) {
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token address '%s'", tokenAddress))
return
}

tokenIds := []*big.Int{}
tokenIdsStr := strings.TrimSpace(c.Query("token_ids"))
if tokenIdsStr != "" {
tokenIds, err = parseTokenIds(c.Query("token_ids"))
if err != nil {
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token ids '%s'", tokenIdsStr))
return
}
}

hideZeroBalances := c.Query("hide_zero_balances") != "false"

columns := []string{"address", "sum(balance) as balance"}
Expand All @@ -77,6 +93,7 @@ func GetTokenBalancesByType(c *gin.Context) {
TokenType: tokenType,
TokenAddress: tokenAddress,
ZeroBalance: hideZeroBalances,
TokenIds: tokenIds,
GroupBy: groupBy,
SortBy: c.Query("sort_by"),
SortOrder: c.Query("sort_order"),
Expand Down Expand Up @@ -131,6 +148,25 @@ func serializeBalance(balance common.TokenBalance) BalanceModel {
}
}

func parseTokenIds(input string) ([]*big.Int, error) {
tokenIdsStr := strings.Split(input, ",")
tokenIdsBn := make([]*big.Int, len(tokenIdsStr))

for i, strNum := range tokenIdsStr {
strNum = strings.TrimSpace(strNum) // Remove potential whitespace
if strNum == "" {
continue // Skip empty strings
}
num := new(big.Int)
_, ok := num.SetString(strNum, 10) // Base 10
if !ok {
return nil, fmt.Errorf("invalid token id: %s", strNum)
}
tokenIdsBn[i] = num
}
return tokenIdsBn, nil
}

// @Summary Get holders of a token
// @Description Retrieve holders of a token
// @Tags holders
Expand Down Expand Up @@ -175,11 +211,22 @@ func GetTokenHoldersByType(c *gin.Context) {
groupBy = []string{"owner", "token_id"}
}

tokenIds := []*big.Int{}
tokenIdsStr := strings.TrimSpace(c.Query("token_ids"))
if tokenIdsStr != "" {
tokenIds, err = parseTokenIds(c.Query("token_ids"))
if err != nil {
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token ids '%s'", tokenIdsStr))
return
}
}

qf := storage.BalancesQueryFilter{
ChainId: chainId,
TokenType: tokenType,
TokenAddress: address,
ZeroBalance: hideZeroBalances,
TokenIds: tokenIds,
GroupBy: groupBy,
SortBy: c.Query("sort_by"),
SortOrder: c.Query("sort_order"),
Expand Down
10 changes: 10 additions & 0 deletions internal/storage/clickhouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,16 @@ func (c *ClickHouseConnector) GetTokenBalances(qf BalancesQueryFilter, fields ..
query += fmt.Sprintf(" AND address = '%s'", qf.TokenAddress)
}

if len(qf.TokenIds) > 0 {
tokenIdsStr := ""
tokenIdsLen := len(qf.TokenIds)
for i := 0; i < tokenIdsLen-1; i++ {
tokenIdsStr += fmt.Sprintf("%s,", qf.TokenIds[i].String())
}
tokenIdsStr += qf.TokenIds[tokenIdsLen-1].String()
query += fmt.Sprintf(" AND token_id in (%s)", tokenIdsStr)
}

isBalanceAggregated := false
for _, field := range fields {
if strings.Contains(field, "balance") && strings.TrimSpace(field) != "balance" {
Expand Down
1 change: 1 addition & 0 deletions internal/storage/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type BalancesQueryFilter struct {
TokenType string
TokenAddress string
Owner string
TokenIds []*big.Int
ZeroBalance bool
GroupBy []string
SortBy string
Expand Down