Skip to content

Commit f33e3df

Browse files
Merge pull request #168 from thirdweb-dev/vt-token-balances-updates
feat: support token id in balances and holders endpoints
2 parents 5b8e1cf + 3726e80 commit f33e3df

File tree

4 files changed

+88
-13
lines changed

4 files changed

+88
-13
lines changed

cmd/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func RunApi(cmd *cobra.Command, args []string) {
8787
// token balance queries
8888
root.GET("/balances/:owner/:type", handlers.GetTokenBalancesByType)
8989

90+
root.GET("/balances/:owner", handlers.GetTokenBalancesByType)
91+
9092
// token holder queries
9193
root.GET("/holders/:address", handlers.GetTokenHoldersByType)
9294

internal/handlers/token_handlers.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"fmt"
5+
"math/big"
56
"strings"
67

78
"github.com/gin-gonic/gin"
@@ -47,11 +48,13 @@ func GetTokenBalancesByType(c *gin.Context) {
4748
api.BadRequestErrorHandler(c, err)
4849
return
4950
}
50-
tokenType := c.Param("type")
51-
if tokenType != "erc20" && tokenType != "erc1155" && tokenType != "erc721" {
52-
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token type '%s'", tokenType))
51+
52+
tokenTypes, err := getTokenTypesFromReq(c)
53+
if err != nil {
54+
api.BadRequestErrorHandler(c, err)
5355
return
5456
}
57+
5558
owner := strings.ToLower(c.Param("owner"))
5659
if !strings.HasPrefix(owner, "0x") {
5760
api.BadRequestErrorHandler(c, fmt.Errorf("invalid owner address '%s'", owner))
@@ -62,21 +65,29 @@ func GetTokenBalancesByType(c *gin.Context) {
6265
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token address '%s'", tokenAddress))
6366
return
6467
}
68+
69+
tokenIds, err := getTokenIdsFromReq(c)
70+
if err != nil {
71+
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token ids '%s'", err))
72+
return
73+
}
74+
6575
hideZeroBalances := c.Query("hide_zero_balances") != "false"
6676

6777
columns := []string{"address", "sum(balance) as balance"}
6878
groupBy := []string{"address"}
69-
if tokenType != "erc20" {
79+
if !strings.Contains(strings.Join(tokenTypes, ","), "erc20") {
7080
columns = []string{"address", "token_id", "sum(balance) as balance"}
7181
groupBy = []string{"address", "token_id"}
7282
}
7383

7484
qf := storage.BalancesQueryFilter{
7585
ChainId: chainId,
7686
Owner: owner,
77-
TokenType: tokenType,
87+
TokenTypes: tokenTypes,
7888
TokenAddress: tokenAddress,
7989
ZeroBalance: hideZeroBalances,
90+
TokenIds: tokenIds,
8091
GroupBy: groupBy,
8192
SortBy: c.Query("sort_by"),
8293
SortOrder: c.Query("sort_order"),
@@ -131,6 +142,43 @@ func serializeBalance(balance common.TokenBalance) BalanceModel {
131142
}
132143
}
133144

145+
func getTokenTypesFromReq(c *gin.Context) ([]string, error) {
146+
tokenTypeParam := c.Param("type")
147+
var tokenTypes []string
148+
if tokenTypeParam != "" {
149+
tokenTypes = []string{tokenTypeParam}
150+
} else {
151+
tokenTypes = c.QueryArray("token_type")
152+
}
153+
154+
for i, tokenType := range tokenTypes {
155+
tokenType = strings.ToLower(tokenType)
156+
if tokenType != "erc721" && tokenType != "erc1155" && tokenType != "erc20" {
157+
return []string{}, fmt.Errorf("invalid token type: %s", tokenType)
158+
}
159+
tokenTypes[i] = tokenType
160+
}
161+
return tokenTypes, nil
162+
}
163+
164+
func getTokenIdsFromReq(c *gin.Context) ([]*big.Int, error) {
165+
tokenIds := c.QueryArray("token_id")
166+
tokenIdsBn := make([]*big.Int, len(tokenIds))
167+
for i, tokenId := range tokenIds {
168+
tokenId = strings.TrimSpace(tokenId) // Remove potential whitespace
169+
if tokenId == "" {
170+
return nil, fmt.Errorf("invalid token id: %s", tokenId)
171+
}
172+
num := new(big.Int)
173+
_, ok := num.SetString(tokenId, 10) // Base 10
174+
if !ok {
175+
return nil, fmt.Errorf("invalid token id: %s", tokenId)
176+
}
177+
tokenIdsBn[i] = num
178+
}
179+
return tokenIdsBn, nil
180+
}
181+
134182
// @Summary Get holders of a token
135183
// @Description Retrieve holders of a token
136184
// @Tags holders
@@ -161,25 +209,32 @@ func GetTokenHoldersByType(c *gin.Context) {
161209
return
162210
}
163211

164-
tokenType := c.Query("token_type")
165-
if tokenType != "" && tokenType != "erc20" && tokenType != "erc1155" && tokenType != "erc721" {
166-
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token type '%s'", tokenType))
212+
tokenTypes, err := getTokenTypesFromReq(c)
213+
if err != nil {
214+
api.BadRequestErrorHandler(c, err)
167215
return
168216
}
169217
hideZeroBalances := c.Query("hide_zero_balances") != "false"
170218

171219
columns := []string{"owner", "sum(balance) as balance"}
172220
groupBy := []string{"owner"}
173-
if tokenType != "erc20" {
221+
222+
if !strings.Contains(strings.Join(tokenTypes, ","), "erc20") {
174223
columns = []string{"owner", "token_id", "sum(balance) as balance"}
175224
groupBy = []string{"owner", "token_id"}
176225
}
177226

227+
tokenIds, err := getTokenIdsFromReq(c)
228+
if err != nil {
229+
api.BadRequestErrorHandler(c, fmt.Errorf("invalid token ids '%s'", err))
230+
return
231+
}
178232
qf := storage.BalancesQueryFilter{
179233
ChainId: chainId,
180-
TokenType: tokenType,
234+
TokenTypes: tokenTypes,
181235
TokenAddress: address,
182236
ZeroBalance: hideZeroBalances,
237+
TokenIds: tokenIds,
183238
GroupBy: groupBy,
184239
SortBy: c.Query("sort_by"),
185240
SortOrder: c.Query("sort_order"),

internal/storage/clickhouse.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,16 +1399,33 @@ func (c *ClickHouseConnector) GetTokenBalances(qf BalancesQueryFilter, fields ..
13991399
}
14001400
query := fmt.Sprintf("SELECT %s FROM %s.token_balances WHERE chain_id = ?", columns, c.cfg.Database)
14011401

1402-
if qf.TokenType != "" {
1403-
query += fmt.Sprintf(" AND token_type = '%s'", qf.TokenType)
1402+
if len(qf.TokenTypes) > 0 {
1403+
tokenTypesStr := ""
1404+
tokenTypesLen := len(qf.TokenTypes)
1405+
for i := 0; i < tokenTypesLen-1; i++ {
1406+
tokenTypesStr += fmt.Sprintf("'%s',", qf.TokenTypes[i])
1407+
}
1408+
tokenTypesStr += fmt.Sprintf("'%s'", qf.TokenTypes[tokenTypesLen-1])
1409+
query += fmt.Sprintf(" AND token_type in (%s)", tokenTypesStr)
14041410
}
1411+
14051412
if qf.Owner != "" {
14061413
query += fmt.Sprintf(" AND owner = '%s'", qf.Owner)
14071414
}
14081415
if qf.TokenAddress != "" {
14091416
query += fmt.Sprintf(" AND address = '%s'", qf.TokenAddress)
14101417
}
14111418

1419+
if len(qf.TokenIds) > 0 {
1420+
tokenIdsStr := ""
1421+
tokenIdsLen := len(qf.TokenIds)
1422+
for i := 0; i < tokenIdsLen-1; i++ {
1423+
tokenIdsStr += fmt.Sprintf("%s,", qf.TokenIds[i].String())
1424+
}
1425+
tokenIdsStr += qf.TokenIds[tokenIdsLen-1].String()
1426+
query += fmt.Sprintf(" AND token_id in (%s)", tokenIdsStr)
1427+
}
1428+
14121429
isBalanceAggregated := false
14131430
for _, field := range fields {
14141431
if strings.Contains(field, "balance") && strings.TrimSpace(field) != "balance" {

internal/storage/connector.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ type QueryFilter struct {
2727

2828
type BalancesQueryFilter struct {
2929
ChainId *big.Int
30-
TokenType string
30+
TokenTypes []string
3131
TokenAddress string
3232
Owner string
33+
TokenIds []*big.Int
3334
ZeroBalance bool
3435
GroupBy []string
3536
SortBy string

0 commit comments

Comments
 (0)