mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-01-12 08:34:28 +08:00
feat: add get Runes info batch api (#73)
* fix: make existing handlers use new total holders usecase * fix: error msg * feat: add get token info batch * feat: add includeHoldersCount in get tokens api * refactor: extract response mapping * fix: rename new field and add holdersCount to extend * fix: query params array * fix: error msg * fix: struct tags * fix: remove error * feat: add default value to additional fields
This commit is contained in:
@@ -3,6 +3,7 @@ package httphandler
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
|||||||
var ok bool
|
var ok bool
|
||||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||||
if !ok {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package httphandler
|
package httphandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"strings"
|
||||||
|
|
||||||
"github.com/cockroachdb/errors"
|
"github.com/cockroachdb/errors"
|
||||||
"github.com/gaze-network/indexer-network/common/errs"
|
"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/indexer-network/modules/runes/runes"
|
||||||
"github.com/gaze-network/uint128"
|
"github.com/gaze-network/uint128"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -14,8 +14,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type getTokenInfoRequest struct {
|
type getTokenInfoRequest struct {
|
||||||
Id string `params:"id"`
|
Id string `params:"id"`
|
||||||
BlockHeight uint64 `query:"blockHeight"`
|
BlockHeight uint64 `query:"blockHeight"`
|
||||||
|
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||||
|
AdditionalFields []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *getTokenInfoRequest) Validate() error {
|
func (r *getTokenInfoRequest) Validate() error {
|
||||||
@@ -28,6 +30,13 @@ func (r *getTokenInfoRequest) Validate() error {
|
|||||||
if !isRuneIdOrRuneName(r.Id) {
|
if !isRuneIdOrRuneName(r.Id) {
|
||||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", 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")
|
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +61,8 @@ type entry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type tokenInfoExtend struct {
|
type tokenInfoExtend struct {
|
||||||
Entry entry `json:"entry"`
|
HoldersCount *int64 `json:"holdersCount,omitempty"`
|
||||||
|
Entry entry `json:"entry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type getTokenInfoResult struct {
|
type getTokenInfoResult struct {
|
||||||
@@ -68,7 +78,7 @@ type getTokenInfoResult struct {
|
|||||||
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
||||||
CompletedAt *int64 `json:"completedAt"` // unix timestamp
|
CompletedAt *int64 `json:"completedAt"` // unix timestamp
|
||||||
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
||||||
HoldersCount int `json:"holdersCount"`
|
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
|
||||||
Extend tokenInfoExtend `json:"extend"`
|
Extend tokenInfoExtend `json:"extend"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +113,7 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
|||||||
var ok bool
|
var ok bool
|
||||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||||
if !ok {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,71 +122,78 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
|||||||
if errors.Is(err, errs.NotFound) {
|
if errors.Is(err, errs.NotFound) {
|
||||||
return errs.NewPublicError("rune not found")
|
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
|
var holdersCountPtr *int64
|
||||||
if err != nil {
|
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||||
if errors.Is(err, errs.NotFound) {
|
holdersCount, err := h.usecase.GetTotalHoldersByRuneId(ctx.UserContext(), runeId, blockHeight)
|
||||||
return errs.NewPublicError("rune not found")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
holdingBalances = lo.Filter(holdingBalances, func(b *entity.Balance, _ int) bool {
|
result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
|
||||||
return !b.Amount.IsZero()
|
|
||||||
})
|
|
||||||
// sort by amount descending
|
|
||||||
slices.SortFunc(holdingBalances, func(i, j *entity.Balance) int {
|
|
||||||
return j.Amount.Cmp(i.Amount)
|
|
||||||
})
|
|
||||||
|
|
||||||
totalSupply, err := runeEntry.Supply()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get total supply of rune")
|
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||||
}
|
}
|
||||||
mintedAmount, err := runeEntry.MintedAmount()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
|
||||||
}
|
|
||||||
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
|
|
||||||
|
|
||||||
terms := lo.FromPtr(runeEntry.Terms)
|
|
||||||
resp := getTokenInfoResponse{
|
resp := getTokenInfoResponse{
|
||||||
Result: &getTokenInfoResult{
|
Result: result,
|
||||||
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 errors.WithStack(ctx.JSON(resp))
|
return errors.WithStack(ctx.JSON(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createTokenInfoResult(runeEntry *runes.RuneEntry, holdersCount *int64) (*getTokenInfoResult, error) {
|
||||||
|
totalSupply, err := runeEntry.Supply()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot get total supply of rune")
|
||||||
|
}
|
||||||
|
mintedAmount, err := runeEntry.MintedAmount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot get minted amount of rune")
|
||||||
|
}
|
||||||
|
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
|
||||||
|
|
||||||
|
terms := lo.FromPtr(runeEntry.Terms)
|
||||||
|
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
@@ -32,22 +32,31 @@ func (s GetTokensScope) IsValid() bool {
|
|||||||
|
|
||||||
type getTokensRequest struct {
|
type getTokensRequest struct {
|
||||||
paginationRequest
|
paginationRequest
|
||||||
Search string `query:"search"`
|
Search string `query:"search"`
|
||||||
BlockHeight uint64 `query:"blockHeight"`
|
BlockHeight uint64 `query:"blockHeight"`
|
||||||
Scope GetTokensScope `query:"scope"`
|
Scope GetTokensScope `query:"scope"`
|
||||||
|
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||||
|
AdditionalFields []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req getTokensRequest) Validate() error {
|
func (r *getTokensRequest) Validate() error {
|
||||||
var errList []error
|
var errList []error
|
||||||
if err := req.paginationRequest.Validate(); err != nil {
|
if err := r.paginationRequest.Validate(); err != nil {
|
||||||
errList = append(errList, err)
|
errList = append(errList, err)
|
||||||
}
|
}
|
||||||
if req.Limit > getTokensMaxLimit {
|
if r.Limit > getTokensMaxLimit {
|
||||||
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
|
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
|
||||||
}
|
}
|
||||||
if req.Scope != "" && !req.Scope.IsValid() {
|
if r.Scope != "" && !r.Scope.IsValid() {
|
||||||
errList = append(errList, errors.Errorf("invalid scope: %s", req.Scope))
|
errList = append(errList, errors.Errorf("invalid scope: %s", r.Scope))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +71,7 @@ func (req *getTokensRequest) ParseDefault() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type getTokensResult struct {
|
type getTokensResult struct {
|
||||||
List []getTokenInfoResult `json:"list"`
|
List []*getTokenInfoResult `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type getTokensResponse = HttpResponse[getTokensResult]
|
type getTokensResponse = HttpResponse[getTokensResult]
|
||||||
@@ -111,62 +120,31 @@ func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
|
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
|
||||||
totalHolders, err := h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
holdersCounts := make(map[runes.RuneId]int64)
|
||||||
if err != nil {
|
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||||
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]getTokenInfoResult, 0, len(entries))
|
results := make([]*getTokenInfoResult, 0, len(entries))
|
||||||
for _, ent := range entries {
|
for _, ent := range entries {
|
||||||
totalSupply, err := ent.Supply()
|
var holdersCount *int64
|
||||||
if err != nil {
|
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||||
return errors.Wrap(err, "cannot get total supply of rune")
|
holdersCount = lo.ToPtr(holdersCounts[ent.RuneId])
|
||||||
}
|
}
|
||||||
mintedAmount, err := ent.MintedAmount()
|
result, err := createTokenInfoResult(ent, holdersCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||||
}
|
}
|
||||||
circulatingSupply := mintedAmount.Sub(ent.BurnedAmount)
|
|
||||||
|
|
||||||
terms := lo.FromPtr(ent.Terms)
|
results = append(results, result)
|
||||||
result = append(result, getTokenInfoResult{
|
|
||||||
Id: ent.RuneId,
|
|
||||||
Name: ent.SpacedRune,
|
|
||||||
Symbol: string(ent.Symbol),
|
|
||||||
TotalSupply: totalSupply,
|
|
||||||
CirculatingSupply: circulatingSupply,
|
|
||||||
MintedAmount: mintedAmount,
|
|
||||||
BurnedAmount: ent.BurnedAmount,
|
|
||||||
Decimals: ent.Divisibility,
|
|
||||||
DeployedAt: ent.EtchedAt.Unix(),
|
|
||||||
DeployedAtHeight: ent.EtchingBlock,
|
|
||||||
CompletedAt: lo.Ternary(ent.CompletedAt.IsZero(), nil, lo.ToPtr(ent.CompletedAt.Unix())),
|
|
||||||
CompletedAtHeight: ent.CompletedAtHeight,
|
|
||||||
HoldersCount: int(totalHolders[ent.RuneId]),
|
|
||||||
Extend: tokenInfoExtend{
|
|
||||||
Entry: entry{
|
|
||||||
Divisibility: ent.Divisibility,
|
|
||||||
Premine: ent.Premine,
|
|
||||||
Rune: ent.SpacedRune.Rune,
|
|
||||||
Spacers: ent.SpacedRune.Spacers,
|
|
||||||
Symbol: string(ent.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: ent.Turbo,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(ctx.JSON(getTokensResponse{
|
return errors.WithStack(ctx.JSON(getTokensResponse{
|
||||||
Result: &getTokensResult{
|
Result: &getTokensResult{
|
||||||
List: result,
|
List: results,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
|||||||
var ok bool
|
var ok bool
|
||||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||||
if !ok {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
|
|||||||
r.Get("/transactions", h.GetTransactions)
|
r.Get("/transactions", h.GetTransactions)
|
||||||
r.Get("/transactions/hash/:hash", h.GetTransactionByHash)
|
r.Get("/transactions/hash/:hash", h.GetTransactionByHash)
|
||||||
r.Get("/holders/:id", h.GetHolders)
|
r.Get("/holders/:id", h.GetHolders)
|
||||||
|
r.Post("/info/batch", h.GetTokenInfoBatch)
|
||||||
r.Get("/info/:id", h.GetTokenInfo)
|
r.Get("/info/:id", h.GetTokenInfo)
|
||||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
|
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
|
||||||
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
|
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
|
||||||
|
|||||||
@@ -33,3 +33,13 @@ func (u *Usecase) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.
|
|||||||
}
|
}
|
||||||
return holders, nil
|
return holders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Usecase) GetTotalHoldersByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (int64, error) {
|
||||||
|
holders, err := u.runesDg.GetTotalHoldersByRuneIds(ctx, []runes.RuneId{runeId}, blockHeight)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to get total holders by rune ids")
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaults to zero holders if not found
|
||||||
|
return holders[runeId], nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ func (u *Usecase) GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId rune
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error) {
|
func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error) {
|
||||||
runeEntry, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
|
runeEntries, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get rune entries by rune ids and height")
|
return nil, errors.Wrap(err, "failed to get rune entries by rune ids and height")
|
||||||
}
|
}
|
||||||
return runeEntry, nil
|
return runeEntries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Usecase) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
|
func (u *Usecase) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user