Skip to content

Commit b2e8ea6

Browse files
authored
improve token balances endpoint performance (#157)
### TL;DR Added balance aggregation support for token balances API endpoint and simplified response model ### What changed? - Modified token balance query to support aggregated balances with GROUP BY functionality - Simplified BalanceModel response by removing redundant fields (chainId, tokenType, owner) - Added serialization functions to transform token balances into the response model - Updated Swagger documentation for the balance endpoint path - Added support for custom column selection in token balance queries - Implemented conditional balance filtering based on aggregation status ### Why make this change? To provide more efficient token balance querying by allowing balance aggregation and reducing redundant data in the response. This improves API performance and simplifies the response structure for consumers.
2 parents c028d57 + fb754f2 commit b2e8ea6

File tree

4 files changed

+91
-25
lines changed

4 files changed

+91
-25
lines changed

internal/handlers/token_handlers.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import (
88
"github.com/gin-gonic/gin"
99
"github.com/rs/zerolog/log"
1010
"github.com/thirdweb-dev/indexer/api"
11+
"github.com/thirdweb-dev/indexer/internal/common"
1112
"github.com/thirdweb-dev/indexer/internal/storage"
1213
)
1314

1415
// BalanceModel return type for Swagger documentation
1516
type BalanceModel struct {
16-
ChainId string `json:"chain_id" ch:"chain_id"`
17-
TokenType string `json:"token_type" ch:"token_type"`
1817
TokenAddress string `json:"token_address" ch:"address"`
19-
Owner string `json:"owner" ch:"owner"`
2018
TokenId string `json:"token_id" ch:"token_id"`
2119
Balance *big.Int `json:"balance" ch:"balance"`
2220
}
@@ -37,7 +35,7 @@ type BalanceModel struct {
3735
// @Failure 400 {object} api.Error
3836
// @Failure 401 {object} api.Error
3937
// @Failure 500 {object} api.Error
40-
// @Router /{chainId}/events [get]
38+
// @Router /{chainId}/balances/{owner}/{type} [get]
4139
func GetTokenBalancesByType(c *gin.Context) {
4240
chainId, err := api.GetChainId(c)
4341
if err != nil {
@@ -60,12 +58,21 @@ func GetTokenBalancesByType(c *gin.Context) {
6058
return
6159
}
6260
hideZeroBalances := c.Query("hide_zero_balances") != "false"
61+
62+
columns := []string{"address", "sum(balance) as balance"}
63+
groupBy := []string{"address"}
64+
if tokenType != "erc20" {
65+
columns = []string{"address", "token_id", "sum(balance) as balance"}
66+
groupBy = []string{"address", "token_id"}
67+
}
68+
6369
qf := storage.BalancesQueryFilter{
6470
ChainId: chainId,
6571
Owner: owner,
6672
TokenType: tokenType,
6773
TokenAddress: tokenAddress,
6874
ZeroBalance: hideZeroBalances,
75+
GroupBy: groupBy,
6976
SortBy: c.Query("sort_by"),
7077
SortOrder: c.Query("sort_order"),
7178
Page: api.ParseIntQueryParam(c.Query("page"), 0),
@@ -87,13 +94,34 @@ func GetTokenBalancesByType(c *gin.Context) {
8794
return
8895
}
8996

90-
balancesResult, err := mainStorage.GetTokenBalances(qf)
97+
balancesResult, err := mainStorage.GetTokenBalances(qf, columns...)
9198
if err != nil {
9299
log.Error().Err(err).Msg("Error querying balances")
93100
// TODO: might want to choose BadRequestError if it's due to not-allowed functions
94101
api.InternalErrorHandler(c)
95102
return
96103
}
97-
queryResult.Data = balancesResult.Data
104+
queryResult.Data = serializeBalances(balancesResult.Data)
98105
sendJSONResponse(c, queryResult)
99106
}
107+
108+
func serializeBalances(balances []common.TokenBalance) []BalanceModel {
109+
balanceModels := make([]BalanceModel, len(balances))
110+
for i, balance := range balances {
111+
balanceModels[i] = serializeBalance(balance)
112+
}
113+
return balanceModels
114+
}
115+
116+
func serializeBalance(balance common.TokenBalance) BalanceModel {
117+
return BalanceModel{
118+
TokenAddress: balance.TokenAddress,
119+
Balance: balance.Balance,
120+
TokenId: func() string {
121+
if balance.TokenId != nil {
122+
return balance.TokenId.String()
123+
}
124+
return ""
125+
}(),
126+
}
127+
}

internal/storage/clickhouse.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,16 +1373,38 @@ func (c *ClickHouseConnector) getTableName(chainId *big.Int, defaultTable string
13731373
return defaultTable
13741374
}
13751375

1376-
func (c *ClickHouseConnector) GetTokenBalances(qf BalancesQueryFilter) (QueryResult[common.TokenBalance], error) {
1376+
func (c *ClickHouseConnector) GetTokenBalances(qf BalancesQueryFilter, fields ...string) (QueryResult[common.TokenBalance], error) {
13771377
columns := "chain_id, token_type, address, owner, token_id, balance"
1378+
if len(fields) > 0 {
1379+
columns = strings.Join(fields, ", ")
1380+
}
1381+
query := fmt.Sprintf("SELECT %s FROM %s.token_balances WHERE chain_id = ? AND token_type = ? AND owner = ?", columns, c.cfg.Database)
1382+
1383+
if qf.TokenAddress != "" {
1384+
query += fmt.Sprintf(" AND address = '%s'", qf.TokenAddress)
1385+
}
1386+
1387+
isBalanceAggregated := false
1388+
for _, field := range fields {
1389+
if strings.Contains(field, "balance") && strings.TrimSpace(field) != "balance" {
1390+
isBalanceAggregated = true
1391+
break
1392+
}
1393+
}
13781394
balanceCondition := ">="
13791395
if qf.ZeroBalance {
13801396
balanceCondition = ">"
13811397
}
1382-
query := fmt.Sprintf("SELECT %s FROM %s.token_balances FINAL WHERE chain_id = ? AND token_type = ? AND owner = ? AND balance %s 0", columns, c.cfg.Database, balanceCondition)
1398+
if !isBalanceAggregated {
1399+
query += fmt.Sprintf(" AND balance %s 0", balanceCondition)
1400+
}
13831401

1384-
if qf.TokenAddress != "" {
1385-
query += fmt.Sprintf(" AND address = '%s'", qf.TokenAddress)
1402+
if len(qf.GroupBy) > 0 {
1403+
query += fmt.Sprintf(" GROUP BY %s", strings.Join(qf.GroupBy, ", "))
1404+
1405+
if isBalanceAggregated {
1406+
query += fmt.Sprintf(" HAVING balance %s 0", balanceCondition)
1407+
}
13861408
}
13871409

13881410
// Add ORDER BY clause

internal/storage/connector.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type BalancesQueryFilter struct {
3131
TokenAddress string
3232
Owner string
3333
ZeroBalance bool
34+
GroupBy []string
3435
SortBy string
3536
SortOrder string
3637
Page int
@@ -80,7 +81,7 @@ type IMainStorage interface {
8081
GetBlockHeadersDescending(chainId *big.Int, from *big.Int, to *big.Int) (blockHeaders []common.BlockHeader, err error)
8182
DeleteBlockData(chainId *big.Int, blockNumbers []*big.Int) error
8283

83-
GetTokenBalances(qf BalancesQueryFilter) (QueryResult[common.TokenBalance], error)
84+
GetTokenBalances(qf BalancesQueryFilter, fields ...string) (QueryResult[common.TokenBalance], error)
8485
}
8586

8687
func NewStorageConnector(cfg *config.StorageConfig) (IStorage, error) {

test/mocks/MockIMainStorage.go

Lines changed: 29 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)