Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add get Runes info batch api #73

Merged
merged 11 commits into from
Oct 31, 2024
3 changes: 2 additions & 1 deletion modules/runes/api/httphandler/get_holders.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package httphandler
import (
"bytes"
"encoding/hex"
"fmt"
"net/url"
"slices"

Expand Down Expand Up @@ -90,7 +91,7 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
var ok bool
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use %q for quotation

}
}

Expand Down
131 changes: 74 additions & 57 deletions modules/runes/api/httphandler/get_token_info.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package httphandler

import (
"fmt"
"net/url"
"slices"
"strings"

"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)

type getTokenInfoRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
AdditionalFields []string
}

func (r *getTokenInfoRequest) Validate() error {
Expand All @@ -28,6 +30,13 @@ func (r *getTokenInfoRequest) Validate() error {
if !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
}

if r.AdditionalFieldsRaw == "" {
// temporarily set default value for backward compatibility
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
}
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")

return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}

Expand All @@ -52,7 +61,8 @@ type entry struct {
}

type tokenInfoExtend struct {
Entry entry `json:"entry"`
HoldersCount *int64 `json:"holdersCount,omitempty"`
Entry entry `json:"entry"`
}

type getTokenInfoResult struct {
Expand All @@ -68,7 +78,7 @@ type getTokenInfoResult struct {
DeployedAtHeight uint64 `json:"deployedAtHeight"`
CompletedAt *int64 `json:"completedAt"` // unix timestamp
CompletedAtHeight *uint64 `json:"completedAtHeight"`
HoldersCount int `json:"holdersCount"`
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
Extend tokenInfoExtend `json:"extend"`
}

Expand Down Expand Up @@ -103,7 +113,7 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
var ok bool
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
}
}

Expand All @@ -112,71 +122,78 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetTokenInfoByHeight")
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
}
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
var holdersCountPtr *int64
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCount, err := h.usecase.GetTotalHoldersByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
holdersCountPtr = &holdersCount
}

result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
if err != nil {
return errors.Wrap(err, "error during createTokenInfoResult")
}

resp := getTokenInfoResponse{
Result: result,
}

holdingBalances = lo.Filter(holdingBalances, func(b *entity.Balance, _ int) bool {
return !b.Amount.IsZero()
})
// sort by amount descending
slices.SortFunc(holdingBalances, func(i, j *entity.Balance) int {
return j.Amount.Cmp(i.Amount)
})
return errors.WithStack(ctx.JSON(resp))
}

func createTokenInfoResult(runeEntry *runes.RuneEntry, holdersCount *int64) (*getTokenInfoResult, error) {
totalSupply, err := runeEntry.Supply()
if err != nil {
return errors.Wrap(err, "cannot get total supply of rune")
return nil, errors.Wrap(err, "cannot get total supply of rune")
}
mintedAmount, err := runeEntry.MintedAmount()
if err != nil {
return errors.Wrap(err, "cannot get minted amount of rune")
return nil, errors.Wrap(err, "cannot get minted amount of rune")
}
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)

terms := lo.FromPtr(runeEntry.Terms)
resp := getTokenInfoResponse{
Result: &getTokenInfoResult{
Id: runeId,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
TotalSupply: totalSupply,
CirculatingSupply: circulatingSupply,
MintedAmount: mintedAmount,
BurnedAmount: runeEntry.BurnedAmount,
Decimals: runeEntry.Divisibility,
DeployedAt: runeEntry.EtchedAt.Unix(),
DeployedAtHeight: runeEntry.EtchingBlock,
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
CompletedAtHeight: runeEntry.CompletedAtHeight,
HoldersCount: len(holdingBalances),
Extend: tokenInfoExtend{
Entry: entry{
Divisibility: runeEntry.Divisibility,
Premine: runeEntry.Premine,
Rune: runeEntry.SpacedRune.Rune,
Spacers: runeEntry.SpacedRune.Spacers,
Symbol: string(runeEntry.Symbol),
Terms: entryTerms{
Amount: lo.FromPtr(terms.Amount),
Cap: lo.FromPtr(terms.Cap),
HeightStart: terms.HeightStart,
HeightEnd: terms.HeightEnd,
OffsetStart: terms.OffsetStart,
OffsetEnd: terms.OffsetEnd,
},
Turbo: runeEntry.Turbo,
EtchingTxHash: runeEntry.EtchingTxHash.String(),

return &getTokenInfoResult{
Id: runeEntry.RuneId,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
TotalSupply: totalSupply,
CirculatingSupply: circulatingSupply,
MintedAmount: mintedAmount,
BurnedAmount: runeEntry.BurnedAmount,
Decimals: runeEntry.Divisibility,
DeployedAt: runeEntry.EtchedAt.Unix(),
DeployedAtHeight: runeEntry.EtchingBlock,
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
CompletedAtHeight: runeEntry.CompletedAtHeight,
HoldersCount: lo.FromPtr(holdersCount),
Extend: tokenInfoExtend{
HoldersCount: holdersCount,
Entry: entry{
Divisibility: runeEntry.Divisibility,
Premine: runeEntry.Premine,
Rune: runeEntry.SpacedRune.Rune,
Spacers: runeEntry.SpacedRune.Spacers,
Symbol: string(runeEntry.Symbol),
Terms: entryTerms{
Amount: lo.FromPtr(terms.Amount),
Cap: lo.FromPtr(terms.Cap),
HeightStart: terms.HeightStart,
HeightEnd: terms.HeightEnd,
OffsetStart: terms.OffsetStart,
OffsetEnd: terms.OffsetEnd,
},
Turbo: runeEntry.Turbo,
EtchingTxHash: runeEntry.EtchingTxHash.String(),
},
},
}

return errors.WithStack(ctx.JSON(resp))
}, nil
}
118 changes: 118 additions & 0 deletions modules/runes/api/httphandler/get_token_info_batch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package httphandler

import (
"fmt"
"net/url"

"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)

type getTokenInfoBatchRequest struct {
Ids []string `json:"ids"`
BlockHeight uint64 `json:"blockHeight"`
AdditionalFields []string `json:"additionalFields"`
}

const getTokenInfoBatchMaxQueries = 100

func (r *getTokenInfoBatchRequest) Validate() error {
var errList []error

if len(r.Ids) == 0 {
errList = append(errList, errors.New("ids cannot be empty"))
}
if len(r.Ids) > getTokenInfoBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot query more than %d ids", getTokenInfoBatchMaxQueries))
}
for i := range r.Ids {
id, err := url.QueryUnescape(r.Ids[i])
if err != nil {
return errors.WithStack(err)
}
r.Ids[i] = id
if !isRuneIdOrRuneName(r.Ids[i]) {
errList = append(errList, errors.Errorf("ids[%d]: id '%s' is not valid rune id or rune name", i, r.Ids[i]))
}
}

return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}

type getTokenInfoBatchResult struct {
List []*getTokenInfoResult `json:"list"`
}
type getTokenInfoBatchResponse = HttpResponse[getTokenInfoBatchResult]

func (h *HttpHandler) GetTokenInfoBatch(ctx *fiber.Ctx) (err error) {
var req getTokenInfoBatchRequest
if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}

blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}

runeIds := make([]runes.RuneId, 0)
for i, id := range req.Ids {
runeId, ok := h.resolveRuneId(ctx.UserContext(), id)
if !ok {
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"ids[%d]\"", id, i))
}
runeIds = append(runeIds, runeId)
}

runeEntries, err := h.usecase.GetRuneEntryByRuneIdAndHeightBatch(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeightBatch")
}
holdersCounts := make(map[runes.RuneId]int64)
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
}

results := make([]*getTokenInfoResult, 0, len(runeIds))

for _, runeId := range runeIds {
runeEntry, ok := runeEntries[runeId]
if !ok {
return errs.NewPublicError(fmt.Sprintf("rune not found: %s", runeId))
}
var holdersCount *int64
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCount = lo.ToPtr(holdersCounts[runeId])
}

result, err := createTokenInfoResult(runeEntry, holdersCount)
if err != nil {
return errors.Wrap(err, "error during createTokenInfoResult")
}
results = append(results, result)
}

resp := getTokenInfoBatchResponse{
Result: &getTokenInfoBatchResult{
List: results,
},
}

return errors.WithStack(ctx.JSON(resp))
}
Loading
Loading