feat: add Runes API pagination (#36)

* feat: implement pagination on get balance, get holders

* feat: paginate get transactions

* fix: remove debug

* feat: implement pagination in get utxos

* feat: sort response in get holders

* feat: cap batch query

* feat: add default limits to all endpoints

* chore: rename endpoint funcs

* fix: parse rune name spacers

* chore: use compare.Cmp

* feat: handle not found errors on all usecase
This commit is contained in:
gazenw
2024-07-23 15:46:45 +07:00
committed by GitHub
parent 880f4b2e6a
commit 7dcbd082ee
20 changed files with 605 additions and 184 deletions

View File

@@ -39,7 +39,7 @@
"ui.completion.usePlaceholders": false, "ui.completion.usePlaceholders": false,
"ui.diagnostic.analyses": { "ui.diagnostic.analyses": {
// https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md // https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md
// "fieldalignment": false, "fieldalignment": false,
"nilness": true, "nilness": true,
"shadow": false, "shadow": false,
"unusedparams": true, "unusedparams": true,

View File

@@ -1,23 +1,29 @@
package httphandler package httphandler
import ( import (
"slices"
"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"
"github.com/samber/lo" "github.com/samber/lo"
) )
type getBalancesByAddressRequest struct { type getBalancesRequest struct {
Wallet string `params:"wallet"` Wallet string `params:"wallet"`
Id string `query:"id"` Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"` BlockHeight uint64 `query:"blockHeight"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
} }
func (r getBalancesByAddressRequest) Validate() error { const (
getBalancesMaxLimit = 5000
getBalancesDefaultLimit = 100
)
func (r getBalancesRequest) Validate() error {
var errList []error var errList []error
if r.Wallet == "" { if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required")) errList = append(errList, errors.New("'wallet' is required"))
@@ -25,6 +31,12 @@ func (r getBalancesByAddressRequest) Validate() error {
if r.Id != "" && !isRuneIdOrRuneName(r.Id) { if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name")) errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
} }
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getBalancesMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getBalancesMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error") return errs.WithPublicMessage(errors.Join(errList...), "validation error")
} }
@@ -36,15 +48,15 @@ type balance struct {
Decimals uint8 `json:"decimals"` Decimals uint8 `json:"decimals"`
} }
type getBalancesByAddressResult struct { type getBalancesResult struct {
List []balance `json:"list"` List []balance `json:"list"`
BlockHeight uint64 `json:"blockHeight"` BlockHeight uint64 `json:"blockHeight"`
} }
type getBalancesByAddressResponse = HttpResponse[getBalancesByAddressResult] type getBalancesResponse = HttpResponse[getBalancesResult]
func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) { func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressRequest var req getBalancesRequest
if err := ctx.ParamsParser(&req); err != nil { if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@@ -54,6 +66,9 @@ func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
if req.Limit == 0 {
req.Limit = getBalancesDefaultLimit
}
pkScript, ok := resolvePkScript(h.network, req.Wallet) pkScript, ok := resolvePkScript(h.network, req.Wallet)
if !ok { if !ok {
@@ -64,49 +79,52 @@ func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
if blockHeight == 0 { if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext()) blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock") return errors.Wrap(err, "error during GetLatestBlock")
} }
blockHeight = uint64(blockHeader.Height) blockHeight = uint64(blockHeader.Height)
} }
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight) balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight, req.Limit, req.Offset)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("balances not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript") return errors.Wrap(err, "error during GetBalancesByPkScript")
} }
runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id) runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id)
if ok { if ok {
// filter out balances that don't match the requested rune id // filter out balances that don't match the requested rune id
for key := range balances { balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
if key != runeId { return b.RuneId == runeId
delete(balances, key) })
}
}
} }
balanceRuneIds := lo.Keys(balances) balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
return b.RuneId
})
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), balanceRuneIds) runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), balanceRuneIds)
if err != nil { if err != nil {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch") return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
} }
balanceList := make([]balance, 0, len(balances)) balanceList := make([]balance, 0, len(balances))
for id, b := range balances { for _, b := range balances {
runeEntry := runeEntries[id] runeEntry := runeEntries[b.RuneId]
balanceList = append(balanceList, balance{ balanceList = append(balanceList, balance{
Amount: b.Amount, Amount: b.Amount,
Id: id, Id: b.RuneId,
Name: runeEntry.SpacedRune, Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol), Symbol: string(runeEntry.Symbol),
Decimals: runeEntry.Divisibility, Decimals: runeEntry.Divisibility,
}) })
} }
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
resp := getBalancesByAddressResponse{ resp := getBalancesResponse{
Result: &getBalancesByAddressResult{ Result: &getBalancesResult{
BlockHeight: blockHeight, BlockHeight: blockHeight,
List: balanceList, List: balanceList,
}, },

View File

@@ -3,10 +3,11 @@ package httphandler
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"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/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/samber/lo" "github.com/samber/lo"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -16,33 +17,49 @@ type getBalanceQuery struct {
Wallet string `json:"wallet"` Wallet string `json:"wallet"`
Id string `json:"id"` Id string `json:"id"`
BlockHeight uint64 `json:"blockHeight"` BlockHeight uint64 `json:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
} }
type getBalancesByAddressBatchRequest struct { type getBalancesBatchRequest struct {
Queries []getBalanceQuery `json:"queries"` Queries []getBalanceQuery `json:"queries"`
} }
func (r getBalancesByAddressBatchRequest) Validate() error { const getBalancesBatchMaxQueries = 100
func (r getBalancesBatchRequest) Validate() error {
var errList []error var errList []error
for _, query := range r.Queries { if len(r.Queries) == 0 {
errList = append(errList, errors.New("at least one query is required"))
}
if len(r.Queries) > getBalancesBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot exceed %d queries", getBalancesBatchMaxQueries))
}
for i, query := range r.Queries {
if query.Wallet == "" { if query.Wallet == "" {
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required")) errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required", i))
} }
if query.Id != "" && !isRuneIdOrRuneName(query.Id) { if query.Id != "" && !isRuneIdOrRuneName(query.Id) {
errList = append(errList, errors.Errorf("queries[%d]: 'id' is not valid rune id or rune name")) errList = append(errList, errors.Errorf("queries[%d]: 'id' is not valid rune id or rune name", i))
}
if query.Limit < 0 {
errList = append(errList, errors.Errorf("queries[%d]: 'limit' must be non-negative", i))
}
if query.Limit > getBalancesMaxLimit {
errList = append(errList, errors.Errorf("queries[%d]: 'limit' cannot exceed %d", i, getBalancesMaxLimit))
} }
} }
return errs.WithPublicMessage(errors.Join(errList...), "validation error") return errs.WithPublicMessage(errors.Join(errList...), "validation error")
} }
type getBalancesByAddressBatchResult struct { type getBalancesBatchResult struct {
List []*getBalancesByAddressResult `json:"list"` List []*getBalancesResult `json:"list"`
} }
type getBalancesByAddressBatchResponse = HttpResponse[getBalancesByAddressBatchResult] type getBalancesBatchResponse = HttpResponse[getBalancesBatchResult]
func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) { func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressBatchRequest var req getBalancesBatchRequest
if err := ctx.BodyParser(&req); err != nil { if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@@ -53,11 +70,14 @@ func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
var latestBlockHeight uint64 var latestBlockHeight uint64
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext()) blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock") return errors.Wrap(err, "error during GetLatestBlock")
} }
latestBlockHeight = uint64(blockHeader.Height) latestBlockHeight = uint64(blockHeader.Height)
processQuery := func(ctx context.Context, query getBalanceQuery, queryIndex int) (*getBalancesByAddressResult, error) { processQuery := func(ctx context.Context, query getBalanceQuery, queryIndex int) (*getBalancesResult, error) {
pkScript, ok := resolvePkScript(h.network, query.Wallet) pkScript, ok := resolvePkScript(h.network, query.Wallet)
if !ok { if !ok {
return nil, errs.NewPublicError(fmt.Sprintf("unable to resolve pkscript from \"queries[%d].wallet\"", queryIndex)) return nil, errs.NewPublicError(fmt.Sprintf("unable to resolve pkscript from \"queries[%d].wallet\"", queryIndex))
@@ -68,50 +88,57 @@ func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
blockHeight = latestBlockHeight blockHeight = latestBlockHeight
} }
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight) if query.Limit == 0 {
query.Limit = getBalancesMaxLimit
}
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight, query.Limit, query.Offset)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return nil, errs.NewPublicError("balances not found")
}
return nil, errors.Wrap(err, "error during GetBalancesByPkScript") return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
} }
runeId, ok := h.resolveRuneId(ctx, query.Id) runeId, ok := h.resolveRuneId(ctx, query.Id)
if ok { if ok {
// filter out balances that don't match the requested rune id // filter out balances that don't match the requested rune id
for key := range balances { balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
if key != runeId { return b.RuneId == runeId
delete(balances, key) })
}
}
} }
balanceRuneIds := lo.Keys(balances) balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
return b.RuneId
})
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx, balanceRuneIds) runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx, balanceRuneIds)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return nil, errs.NewPublicError("rune not found")
}
return nil, errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch") return nil, errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
} }
balanceList := make([]balance, 0, len(balances)) balanceList := make([]balance, 0, len(balances))
for id, b := range balances { for _, b := range balances {
runeEntry := runeEntries[id] runeEntry := runeEntries[b.RuneId]
balanceList = append(balanceList, balance{ balanceList = append(balanceList, balance{
Amount: b.Amount, Amount: b.Amount,
Id: id, Id: b.RuneId,
Name: runeEntry.SpacedRune, Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol), Symbol: string(runeEntry.Symbol),
Decimals: runeEntry.Divisibility, Decimals: runeEntry.Divisibility,
}) })
} }
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
result := getBalancesByAddressResult{ result := getBalancesResult{
BlockHeight: blockHeight, BlockHeight: blockHeight,
List: balanceList, List: balanceList,
} }
return &result, nil return &result, nil
} }
results := make([]*getBalancesByAddressResult, len(req.Queries)) results := make([]*getBalancesResult, len(req.Queries))
eg, ectx := errgroup.WithContext(ctx.UserContext()) eg, ectx := errgroup.WithContext(ctx.UserContext())
for i, query := range req.Queries { for i, query := range req.Queries {
i := i i := i
@@ -129,8 +156,8 @@ func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
return errors.WithStack(err) return errors.WithStack(err)
} }
resp := getBalancesByAddressBatchResponse{ resp := getBalancesBatchResponse{
Result: &getBalancesByAddressBatchResult{ Result: &getBalancesBatchResult{
List: results, List: results,
}, },
} }

View File

@@ -1,10 +1,13 @@
package httphandler package httphandler
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"slices"
"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,13 +17,26 @@ import (
type getHoldersRequest struct { type getHoldersRequest struct {
Id string `params:"id"` Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"` BlockHeight uint64 `query:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
} }
const (
getHoldersMaxLimit = 1000
getHoldersDefaultLimit = 100
)
func (r getHoldersRequest) Validate() error { func (r getHoldersRequest) Validate() error {
var errList []error var errList []error
if !isRuneIdOrRuneName(r.Id) { if !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name")) errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
} }
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getHoldersMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getHoldersMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error") return errs.WithPublicMessage(errors.Join(errList...), "validation error")
} }
@@ -61,6 +77,10 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
blockHeight = uint64(blockHeader.Height) blockHeight = uint64(blockHeader.Height)
} }
if req.Limit == 0 {
req.Limit = getHoldersDefaultLimit
}
var runeId runes.RuneId var runeId runes.RuneId
if req.Id != "" { if req.Id != "" {
var ok bool var ok bool
@@ -75,10 +95,13 @@ func (h *HttpHandler) GetHolders(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 GetHoldersByHeight") return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
} }
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight) holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, req.Limit, req.Offset)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("balances not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId") return errors.Wrap(err, "error during GetBalancesByRuneId")
} }
@@ -104,6 +127,14 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
}) })
} }
// sort by amount descending, then pk script ascending
slices.SortFunc(holdingBalances, func(b1, b2 *entity.Balance) int {
if b1.Amount.Cmp(b2.Amount) == 0 {
return bytes.Compare(b1.PkScript, b2.PkScript)
}
return b2.Amount.Cmp(b1.Amount)
})
resp := getHoldersResponse{ resp := getHoldersResponse{
Result: &getHoldersResult{ Result: &getHoldersResult{
BlockHeight: blockHeight, BlockHeight: blockHeight,

View File

@@ -83,6 +83,9 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
if blockHeight == 0 { if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext()) blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock") return errors.Wrap(err, "error during GetLatestBlock")
} }
blockHeight = uint64(blockHeader.Height) blockHeight = uint64(blockHeader.Height)
@@ -104,8 +107,11 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
} }
return errors.Wrap(err, "error during GetTokenInfoByHeight") return errors.Wrap(err, "error during GetTokenInfoByHeight")
} }
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight) holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
if err != nil { 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")
} }

View File

@@ -1,6 +1,7 @@
package httphandler package httphandler
import ( import (
"cmp"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"slices" "slices"
@@ -15,13 +16,19 @@ import (
) )
type getTransactionsRequest struct { type getTransactionsRequest struct {
Wallet string `query:"wallet"` Wallet string `query:"wallet"`
Id string `query:"id"` Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
FromBlock int64 `query:"fromBlock"` ToBlock int64 `query:"toBlock"`
ToBlock int64 `query:"toBlock"` Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
} }
const (
getTransactionsMaxLimit = 3000
getTransactionsDefaultLimit = 100
)
func (r getTransactionsRequest) Validate() error { func (r getTransactionsRequest) Validate() error {
var errList []error var errList []error
if r.Id != "" && !isRuneIdOrRuneName(r.Id) { if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
@@ -33,6 +40,12 @@ func (r getTransactionsRequest) Validate() error {
if r.ToBlock < -1 { if r.ToBlock < -1 {
errList = append(errList, errors.Errorf("invalid toBlock range")) errList = append(errList, errors.Errorf("invalid toBlock range"))
} }
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getTransactionsMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getTransactionsMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error") return errs.WithPublicMessage(errors.Join(errList...), "validation error")
} }
@@ -133,6 +146,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError("unable to resolve rune id from \"id\"") return errs.NewPublicError("unable to resolve rune id from \"id\"")
} }
} }
if req.Limit == 0 {
req.Limit = getTransactionsDefaultLimit
}
// default to latest block // default to latest block
if req.ToBlock == 0 { if req.ToBlock == 0 {
@@ -143,6 +159,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
if req.FromBlock == -1 || req.ToBlock == -1 { if req.FromBlock == -1 || req.ToBlock == -1 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext()) blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock") return errors.Wrap(err, "error during GetLatestBlock")
} }
if req.FromBlock == -1 { if req.FromBlock == -1 {
@@ -158,8 +177,11 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError(fmt.Sprintf("fromBlock must be less than or equal to toBlock, got fromBlock=%d, toBlock=%d", req.FromBlock, req.ToBlock)) return errs.NewPublicError(fmt.Sprintf("fromBlock must be less than or equal to toBlock, got fromBlock=%d, toBlock=%d", req.FromBlock, req.ToBlock))
} }
txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, uint64(req.FromBlock), uint64(req.ToBlock)) txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, uint64(req.FromBlock), uint64(req.ToBlock), req.Limit, req.Offset)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("transactions not found")
}
return errors.Wrap(err, "error during GetRuneTransactions") return errors.Wrap(err, "error during GetRuneTransactions")
} }
@@ -181,6 +203,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
allRuneIds = lo.Uniq(allRuneIds) allRuneIds = lo.Uniq(allRuneIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds) runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch") return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
} }
@@ -279,12 +304,12 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
} }
txList = append(txList, respTx) txList = append(txList, respTx)
} }
// sort by block height ASC, then index ASC // sort by block height DESC, then index DESC
slices.SortFunc(txList, func(t1, t2 transaction) int { slices.SortFunc(txList, func(t1, t2 transaction) int {
if t1.BlockHeight != t2.BlockHeight { if t1.BlockHeight != t2.BlockHeight {
return int(t1.BlockHeight - t2.BlockHeight) return cmp.Compare(t2.BlockHeight, t1.BlockHeight)
} }
return int(t1.Index - t2.Index) return cmp.Compare(t2.Index, t1.Index)
}) })
resp := getTransactionsResponse{ resp := getTransactionsResponse{

View File

@@ -2,7 +2,6 @@ package httphandler
import ( import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"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/internal/entity"
@@ -12,13 +11,20 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
type getUTXOsByAddressRequest struct { type getUTXOsRequest struct {
Wallet string `params:"wallet"` Wallet string `params:"wallet"`
Id string `query:"id"` Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"` BlockHeight uint64 `query:"blockHeight"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
} }
func (r getUTXOsByAddressRequest) Validate() error { const (
getUTXOsMaxLimit = 3000
getUTXOsDefaultLimit = 100
)
func (r getUTXOsRequest) Validate() error {
var errList []error var errList []error
if r.Wallet == "" { if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required")) errList = append(errList, errors.New("'wallet' is required"))
@@ -26,6 +32,12 @@ func (r getUTXOsByAddressRequest) Validate() error {
if r.Id != "" && !isRuneIdOrRuneName(r.Id) { if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name")) errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
} }
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getUTXOsMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getUTXOsMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error") return errs.WithPublicMessage(errors.Join(errList...), "validation error")
} }
@@ -41,21 +53,21 @@ type utxoExtend struct {
Runes []runeBalance `json:"runes"` Runes []runeBalance `json:"runes"`
} }
type utxo struct { type utxoItem struct {
TxHash chainhash.Hash `json:"txHash"` TxHash chainhash.Hash `json:"txHash"`
OutputIndex uint32 `json:"outputIndex"` OutputIndex uint32 `json:"outputIndex"`
Extend utxoExtend `json:"extend"` Extend utxoExtend `json:"extend"`
} }
type getUTXOsByAddressResult struct { type getUTXOsResult struct {
List []utxo `json:"list"` List []utxoItem `json:"list"`
BlockHeight uint64 `json:"blockHeight"` BlockHeight uint64 `json:"blockHeight"`
} }
type getUTXOsByAddressResponse = HttpResponse[getUTXOsByAddressResult] type getUTXOsResponse = HttpResponse[getUTXOsResult]
func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) { func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
var req getUTXOsByAddressRequest var req getUTXOsRequest
if err := ctx.ParamsParser(&req); err != nil { if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@@ -71,36 +83,60 @@ func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"") return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
} }
if req.Limit == 0 {
req.Limit = getUTXOsDefaultLimit
}
blockHeight := req.BlockHeight blockHeight := req.BlockHeight
if blockHeight == 0 { if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext()) blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock") return errors.Wrap(err, "error during GetLatestBlock")
} }
blockHeight = uint64(blockHeader.Height) blockHeight = uint64(blockHeader.Height)
} }
outPointBalances, err := h.usecase.GetUnspentOutPointBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight) var utxos []*entity.RunesUTXO
if err != nil { if runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id); ok {
return errors.Wrap(err, "error during GetBalancesByPkScript") utxos, err = h.usecase.GetRunesUTXOsByRuneIdAndPkScript(ctx.UserContext(), runeId, pkScript, blockHeight, req.Limit, req.Offset)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("utxos not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
} else {
utxos, err = h.usecase.GetRunesUTXOsByPkScript(ctx.UserContext(), pkScript, blockHeight, req.Limit, req.Offset)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("utxos not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
} }
outPointBalanceRuneIds := lo.Map(outPointBalances, func(outPointBalance *entity.OutPointBalance, _ int) runes.RuneId { runeIds := make(map[runes.RuneId]struct{}, 0)
return outPointBalance.RuneId for _, utxo := range utxos {
}) for _, balance := range utxo.RuneBalances {
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), outPointBalanceRuneIds) runeIds[balance.RuneId] = struct{}{}
}
}
runeIdsList := lo.Keys(runeIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), runeIdsList)
if err != nil { if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch") return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
} }
groupedBalances := lo.GroupBy(outPointBalances, func(outPointBalance *entity.OutPointBalance) wire.OutPoint { utxoRespList := make([]utxoItem, 0, len(utxos))
return outPointBalance.OutPoint for _, utxo := range utxos {
}) runeBalances := make([]runeBalance, 0, len(utxo.RuneBalances))
for _, balance := range utxo.RuneBalances {
utxoList := make([]utxo, 0, len(groupedBalances))
for outPoint, balances := range groupedBalances {
runeBalances := make([]runeBalance, 0, len(balances))
for _, balance := range balances {
runeEntry := runeEntries[balance.RuneId] runeEntry := runeEntries[balance.RuneId]
runeBalances = append(runeBalances, runeBalance{ runeBalances = append(runeBalances, runeBalance{
RuneId: balance.RuneId, RuneId: balance.RuneId,
@@ -111,34 +147,19 @@ func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) {
}) })
} }
utxoList = append(utxoList, utxo{ utxoRespList = append(utxoRespList, utxoItem{
TxHash: outPoint.Hash, TxHash: utxo.OutPoint.Hash,
OutputIndex: outPoint.Index, OutputIndex: utxo.OutPoint.Index,
Extend: utxoExtend{ Extend: utxoExtend{
Runes: runeBalances, Runes: runeBalances,
}, },
}) })
} }
// filter by req.Id if exists resp := getUTXOsResponse{
{ Result: &getUTXOsResult{
runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id)
if ok {
utxoList = lo.Filter(utxoList, func(u utxo, _ int) bool {
for _, runeBalance := range u.Extend.Runes {
if runeBalance.RuneId == runeId {
return true
}
}
return false
})
}
}
resp := getUTXOsByAddressResponse{
Result: &getUTXOsByAddressResult{
BlockHeight: blockHeight, BlockHeight: blockHeight,
List: utxoList, List: utxoRespList,
}, },
} }

View File

@@ -7,12 +7,12 @@ import (
func (h *HttpHandler) Mount(router fiber.Router) error { func (h *HttpHandler) Mount(router fiber.Router) error {
r := router.Group("/v2/runes") r := router.Group("/v2/runes")
r.Post("/balances/wallet/batch", h.GetBalancesByAddressBatch) r.Post("/balances/wallet/batch", h.GetBalancesBatch)
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress) r.Get("/balances/wallet/:wallet", h.GetBalances)
r.Get("/transactions", h.GetTransactions) r.Get("/transactions", h.GetTransactions)
r.Get("/holders/:id", h.GetHolders) r.Get("/holders/:id", h.GetHolders)
r.Get("/info/:id", h.GetTokenInfo) r.Get("/info/:id", h.GetTokenInfo)
r.Get("/utxos/wallet/:wallet", h.GetUTXOsByAddress) r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
r.Get("/block", h.GetCurrentBlock) r.Get("/block", h.GetCurrentBlock)
return nil return nil
} }

View File

@@ -118,5 +118,7 @@ CREATE TABLE IF NOT EXISTS "runes_balances" (
"amount" DECIMAL NOT NULL, "amount" DECIMAL NOT NULL,
PRIMARY KEY ("pkscript", "rune_id", "block_height") PRIMARY KEY ("pkscript", "rune_id", "block_height")
); );
CREATE INDEX IF NOT EXISTS runes_balances_rune_id_block_height_idx ON "runes_balances" USING BTREE ("rune_id", "block_height");
CREATE INDEX IF NOT EXISTS runes_balances_pkscript_block_height_idx ON "runes_balances" USING BTREE ("pkscript", "block_height");
COMMIT; COMMIT;

View File

@@ -2,13 +2,13 @@
WITH balances AS ( WITH balances AS (
SELECT DISTINCT ON (rune_id) * FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC SELECT DISTINCT ON (rune_id) * FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
) )
SELECT * FROM balances WHERE amount > 0; SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4;
-- name: GetBalancesByRuneId :many -- name: GetBalancesByRuneId :many
WITH balances AS ( WITH balances AS (
SELECT DISTINCT ON (pkscript) * FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC SELECT DISTINCT ON (pkscript) * FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
) )
SELECT * FROM balances WHERE amount > 0; SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4;
-- name: GetBalanceByPkScriptAndRuneId :one -- name: GetBalanceByPkScriptAndRuneId :one
SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_height <= $3 ORDER BY block_height DESC LIMIT 1; SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_height <= $3 ORDER BY block_height DESC LIMIT 1;
@@ -16,8 +16,28 @@ SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_heig
-- name: GetOutPointBalancesAtOutPoint :many -- name: GetOutPointBalancesAtOutPoint :many
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2; SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
-- name: GetUnspentOutPointBalancesByPkScript :many -- name: GetRunesUTXOsByPkScript :many
SELECT * FROM runes_outpoint_balances WHERE pkscript = @pkScript AND block_height <= @block_height AND (spent_height IS NULL OR spent_height > @block_height); SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> @rune_ids::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetRuneEntriesByRuneIds :many -- name: GetRuneEntriesByRuneIds :many
WITH states AS ( WITH states AS (
@@ -57,7 +77,7 @@ SELECT * FROM runes_transactions
) AND ( ) AND (
@from_block <= runes_transactions.block_height AND runes_transactions.block_height <= @to_block @from_block <= runes_transactions.block_height AND runes_transactions.block_height <= @to_block
) )
ORDER BY runes_transactions.block_height DESC LIMIT 10000; ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2;
-- name: CountRuneEntries :one -- name: CountRuneEntries :one
SELECT COUNT(*) FROM runes_entries; SELECT COUNT(*) FROM runes_entries;

View File

@@ -27,10 +27,11 @@ type RunesReaderDataGateway interface {
GetLatestBlock(ctx context.Context) (types.BlockHeader, error) GetLatestBlock(ctx context.Context) (types.BlockHeader, error)
GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error) GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error)
// GetRuneTransactions returns the runes transactions, filterable by pkScript, runeId and height. If pkScript, runeId or height is zero value, that filter is ignored. // GetRuneTransactions returns the runes transactions, filterable by pkScript, runeId and height. If pkScript, runeId or height is zero value, that filter is ignored.
GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error)
GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error)
GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
// GetRuneIdFromRune returns the RuneId for the given rune. Returns errs.NotFound if the rune entry is not found. // GetRuneIdFromRune returns the RuneId for the given rune. Returns errs.NotFound if the rune entry is not found.
GetRuneIdFromRune(ctx context.Context, rune runes.Rune) (runes.RuneId, error) GetRuneIdFromRune(ctx context.Context, rune runes.Rune) (runes.RuneId, error)
// GetRuneEntryByRuneId returns the RuneEntry for the given runeId. Returns errs.NotFound if the rune entry is not found. // GetRuneEntryByRuneId returns the RuneEntry for the given runeId. Returns errs.NotFound if the rune entry is not found.
@@ -45,10 +46,12 @@ type RunesReaderDataGateway interface {
CountRuneEntries(ctx context.Context) (uint64, error) CountRuneEntries(ctx context.Context) (uint64, error)
// GetBalancesByPkScript returns the balances for the given pkScript at the given blockHeight. // GetBalancesByPkScript returns the balances for the given pkScript at the given blockHeight.
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) // Use limit = -1 as no limit.
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
// GetBalancesByRuneId returns the balances for the given runeId at the given blockHeight. // GetBalancesByRuneId returns the balances for the given runeId at the given blockHeight.
// Cannot use []byte as map key, so we're returning as slice. // Cannot use []byte as map key, so we're returning as slice.
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) // Use limit = -1 as no limit.
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
// GetBalancesByPkScriptAndRuneId returns the balance for the given pkScript and runeId at the given blockHeight. // GetBalancesByPkScriptAndRuneId returns the balance for the given pkScript and runeId at the given blockHeight.
GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript []byte, runeId runes.RuneId, blockHeight uint64) (*entity.Balance, error) GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript []byte, runeId runes.RuneId, blockHeight uint64) (*entity.Balance, error)
} }

View File

@@ -0,0 +1,18 @@
package entity
import (
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
)
type RunesUTXOBalance struct {
RuneId runes.RuneId
Amount uint128.Uint128
}
type RunesUTXO struct {
PkScript []byte
OutPoint wire.OutPoint
RuneBalances []RunesUTXOBalance
}

View File

@@ -296,12 +296,14 @@ const getBalancesByPkScript = `-- name: GetBalancesByPkScript :many
WITH balances AS ( WITH balances AS (
SELECT DISTINCT ON (rune_id) pkscript, block_height, rune_id, amount FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC SELECT DISTINCT ON (rune_id) pkscript, block_height, rune_id, amount FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
) )
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4
` `
type GetBalancesByPkScriptParams struct { type GetBalancesByPkScriptParams struct {
Pkscript string Pkscript string
BlockHeight int32 BlockHeight int32
Limit int32
Offset int32
} }
type GetBalancesByPkScriptRow struct { type GetBalancesByPkScriptRow struct {
@@ -312,7 +314,12 @@ type GetBalancesByPkScriptRow struct {
} }
func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) { func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getBalancesByPkScript, arg.Pkscript, arg.BlockHeight) rows, err := q.db.Query(ctx, getBalancesByPkScript,
arg.Pkscript,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -340,12 +347,14 @@ const getBalancesByRuneId = `-- name: GetBalancesByRuneId :many
WITH balances AS ( WITH balances AS (
SELECT DISTINCT ON (pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC SELECT DISTINCT ON (pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
) )
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4
` `
type GetBalancesByRuneIdParams struct { type GetBalancesByRuneIdParams struct {
RuneID string RuneID string
BlockHeight int32 BlockHeight int32
Limit int32
Offset int32
} }
type GetBalancesByRuneIdRow struct { type GetBalancesByRuneIdRow struct {
@@ -356,7 +365,12 @@ type GetBalancesByRuneIdRow struct {
} }
func (q *Queries) GetBalancesByRuneId(ctx context.Context, arg GetBalancesByRuneIdParams) ([]GetBalancesByRuneIdRow, error) { func (q *Queries) GetBalancesByRuneId(ctx context.Context, arg GetBalancesByRuneIdParams) ([]GetBalancesByRuneIdRow, error) {
rows, err := q.db.Query(ctx, getBalancesByRuneId, arg.RuneID, arg.BlockHeight) rows, err := q.db.Query(ctx, getBalancesByRuneId,
arg.RuneID,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -635,23 +649,25 @@ const getRuneTransactions = `-- name: GetRuneTransactions :many
SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws FROM runes_transactions SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE ( WHERE (
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter $3::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR runes_transactions.outputs @> $2::JSONB
OR runes_transactions.inputs @> $2::JSONB
) AND (
$3::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $4::JSONB OR runes_transactions.outputs @> $4::JSONB
OR runes_transactions.inputs @> $4::JSONB OR runes_transactions.inputs @> $4::JSONB
OR runes_transactions.mints ? $5
OR runes_transactions.burns ? $5
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $6 AND runes_transactions.index = $7)
) AND ( ) AND (
$8 <= runes_transactions.block_height AND runes_transactions.block_height <= $9 $5::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $6::JSONB
OR runes_transactions.inputs @> $6::JSONB
OR runes_transactions.mints ? $7
OR runes_transactions.burns ? $7
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $8 AND runes_transactions.index = $9)
) AND (
$10 <= runes_transactions.block_height AND runes_transactions.block_height <= $11
) )
ORDER BY runes_transactions.block_height DESC LIMIT 10000 ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2
` `
type GetRuneTransactionsParams struct { type GetRuneTransactionsParams struct {
Limit int32
Offset int32
FilterPkScript bool FilterPkScript bool
PkScriptParam []byte PkScriptParam []byte
FilterRuneID bool FilterRuneID bool
@@ -698,6 +714,8 @@ type GetRuneTransactionsRow struct {
func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactionsParams) ([]GetRuneTransactionsRow, error) { func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactionsParams) ([]GetRuneTransactionsRow, error) {
rows, err := q.db.Query(ctx, getRuneTransactions, rows, err := q.db.Query(ctx, getRuneTransactions,
arg.Limit,
arg.Offset,
arg.FilterPkScript, arg.FilterPkScript,
arg.PkScriptParam, arg.PkScriptParam,
arg.FilterRuneID, arg.FilterRuneID,
@@ -757,32 +775,114 @@ func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactio
return items, nil return items, nil
} }
const getUnspentOutPointBalancesByPkScript = `-- name: GetUnspentOutPointBalancesByPkScript :many const getRunesUTXOsByPkScript = `-- name: GetRunesUTXOsByPkScript :many
SELECT rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height FROM runes_outpoint_balances WHERE pkscript = $1 AND block_height <= $2 AND (spent_height IS NULL OR spent_height > $2) SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
` `
type GetUnspentOutPointBalancesByPkScriptParams struct { type GetRunesUTXOsByPkScriptParams struct {
Limit int32
Offset int32
Pkscript string Pkscript string
BlockHeight int32 BlockHeight int32
} }
func (q *Queries) GetUnspentOutPointBalancesByPkScript(ctx context.Context, arg GetUnspentOutPointBalancesByPkScriptParams) ([]RunesOutpointBalance, error) { type GetRunesUTXOsByPkScriptRow struct {
rows, err := q.db.Query(ctx, getUnspentOutPointBalancesByPkScript, arg.Pkscript, arg.BlockHeight) TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByPkScript(ctx context.Context, arg GetRunesUTXOsByPkScriptParams) ([]GetRunesUTXOsByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []RunesOutpointBalance var items []GetRunesUTXOsByPkScriptRow
for rows.Next() { for rows.Next() {
var i RunesOutpointBalance var i GetRunesUTXOsByPkScriptRow
if err := rows.Scan( if err := rows.Scan(
&i.RuneID,
&i.Pkscript,
&i.TxHash, &i.TxHash,
&i.TxIdx, &i.TxIdx,
&i.Amount, &i.Pkscript,
&i.BlockHeight, &i.RuneIds,
&i.SpentHeight, &i.Amounts,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRunesUTXOsByRuneIdAndPkScript = `-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> $5::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
`
type GetRunesUTXOsByRuneIdAndPkScriptParams struct {
Limit int32
Offset int32
Pkscript string
BlockHeight int32
RuneIds []string
}
type GetRunesUTXOsByRuneIdAndPkScriptRow struct {
TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetRunesUTXOsByRuneIdAndPkScriptParams) ([]GetRunesUTXOsByRuneIdAndPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByRuneIdAndPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
arg.RuneIds,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunesUTXOsByRuneIdAndPkScriptRow
for rows.Next() {
var i GetRunesUTXOsByRuneIdAndPkScriptRow
if err := rows.Scan(
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.RuneIds,
&i.Amounts,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@@ -638,6 +638,72 @@ func mapIndexedBlockTypeToParams(src entity.IndexedBlock) (gen.CreateIndexedBloc
}, nil }, nil
} }
func mapRunesUTXOModelToType(src gen.GetRunesUTXOsByPkScriptRow) (entity.RunesUTXO, error) {
pkScriptRaw, ok := src.Pkscript.(string)
if !ok {
return entity.RunesUTXO{}, errors.New("pkscript from database is not string")
}
pkScript, err := hex.DecodeString(pkScriptRaw)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse pkscript")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse tx hash")
}
runeIdsRaw, ok := src.RuneIds.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("src.RuneIds is not a slice")
}
runeIds := make([]string, 0, len(runeIdsRaw))
for i, raw := range runeIdsRaw {
runeId, ok := raw.(string)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.RuneIds[%d] is not a string", i)
}
runeIds = append(runeIds, runeId)
}
amountsRaw, ok := src.Amounts.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("amounts from database is not a slice")
}
amounts := make([]pgtype.Numeric, 0, len(amountsRaw))
for i, raw := range amountsRaw {
amount, ok := raw.(pgtype.Numeric)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.Amounts[%d] is not pgtype.Numeric", i)
}
amounts = append(amounts, amount)
}
if len(runeIds) != len(amounts) {
return entity.RunesUTXO{}, errors.New("rune ids and amounts have different lengths")
}
runesBalances := make([]entity.RunesUTXOBalance, 0, len(runeIds))
for i := range runeIds {
runeId, err := runes.NewRuneIdFromString(runeIds[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse rune id")
}
amount, err := uint128FromNumeric(amounts[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse amount")
}
runesBalances = append(runesBalances, entity.RunesUTXOBalance{
RuneId: runeId,
Amount: lo.FromPtr(amount),
})
}
return entity.RunesUTXO{
PkScript: pkScript,
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.TxIdx),
},
RuneBalances: runesBalances,
}, nil
}
func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPointBalance, error) { func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPointBalance, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID) runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@@ -62,7 +63,18 @@ func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64)
return indexedBlock, nil return indexedBlock, nil
} }
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) { const maxRuneTransactionsLimit = 10000 // temporary limit to prevent large queries from overwhelming the database
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
if limit == -1 {
limit = maxRuneTransactionsLimit
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
if limit > maxRuneTransactionsLimit {
return nil, errors.Wrapf(errs.InvalidArgument, "limit cannot exceed %d", maxRuneTransactionsLimit)
}
pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript))) pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript)))
runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String())) runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String()))
rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{ rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{
@@ -77,6 +89,9 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
FromBlock: int32(fromBlock), FromBlock: int32(fromBlock),
ToBlock: int32(toBlock), ToBlock: int32(toBlock),
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during query") return nil, errors.Wrap(err, "error during query")
@@ -125,22 +140,59 @@ func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wi
return result, nil return result, nil
} }
func (r *Repository) GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error) { func (r *Repository) GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
balances, err := r.queries.GetUnspentOutPointBalancesByPkScript(ctx, gen.GetUnspentOutPointBalancesByPkScriptParams{ if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByPkScript(ctx, gen.GetRunesUTXOsByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript), Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight), BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during query") return nil, errors.Wrap(err, "error during query")
} }
result := make([]*entity.OutPointBalance, 0, len(balances)) result := make([]*entity.RunesUTXO, 0, len(rows))
for _, balanceModel := range balances { for _, row := range rows {
balance, err := mapOutPointBalanceModelToType(balanceModel) utxo, err := mapRunesUTXOModelToType(row)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model") return nil, errors.Wrap(err, "failed to parse row model")
} }
result = append(result, &balance) result = append(result, &utxo)
}
return result, nil
}
func (r *Repository) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByRuneIdAndPkScript(ctx, gen.GetRunesUTXOsByRuneIdAndPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
RuneIds: []string{runeId.String()},
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.RunesUTXO, 0, len(rows))
for _, row := range rows {
utxo, err := mapRunesUTXOModelToType(gen.GetRunesUTXOsByPkScriptRow(row))
if err != nil {
return nil, errors.Wrap(err, "failed to parse row")
}
result = append(result, &utxo)
} }
return result, nil return result, nil
} }
@@ -245,30 +297,46 @@ func (r *Repository) CountRuneEntries(ctx context.Context) (uint64, error) {
return uint64(count), nil return uint64(count), nil
} }
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) { func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
balances, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{ balances, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript), Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight), BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during query") return nil, errors.Wrap(err, "error during query")
} }
result := make(map[runes.RuneId]*entity.Balance, len(balances)) result := make([]*entity.Balance, 0, len(balances))
for _, balanceModel := range balances { for _, balanceModel := range balances {
balance, err := mapBalanceModelToType(gen.RunesBalance(balanceModel)) balance, err := mapBalanceModelToType(gen.RunesBalance(balanceModel))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model") return nil, errors.Wrap(err, "failed to parse balance model")
} }
result[balance.RuneId] = balance result = append(result, balance)
} }
return result, nil return result, nil
} }
func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) { func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
balances, err := r.queries.GetBalancesByRuneId(ctx, gen.GetBalancesByRuneIdParams{ balances, err := r.queries.GetBalancesByRuneId(ctx, gen.GetBalancesByRuneIdParams{
RuneID: runeId.String(), RuneID: runeId.String(),
BlockHeight: int32(blockHeight), BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during query") return nil, errors.Wrap(err, "error during query")

View File

@@ -29,6 +29,10 @@ var ErrInvalidBase26 = errors.New("invalid base-26 character: must be in the ran
func NewRuneFromString(value string) (Rune, error) { func NewRuneFromString(value string) (Rune, error) {
n := uint128.From64(0) n := uint128.From64(0)
for i, char := range value { for i, char := range value {
// skip spacers
if char == '.' || char == '•' {
continue
}
if i > 0 { if i > 0 {
n = n.Add(uint128.From64(1)) n = n.Add(uint128.From64(1))
} }

View File

@@ -8,16 +8,18 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/runes" "github.com/gaze-network/indexer-network/modules/runes/runes"
) )
func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) { // Use limit = -1 as no limit.
balances, err := u.runesDg.GetBalancesByPkScript(ctx, pkScript, blockHeight) func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByPkScript(ctx, pkScript, blockHeight, limit, offset)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript") return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
} }
return balances, nil return balances, nil
} }
func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) { // Use limit = -1 as no limit.
balances, err := u.runesDg.GetBalancesByRuneId(ctx, runeId, blockHeight) func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByRuneId(ctx, runeId, blockHeight, limit, offset)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get rune holders by rune id") return nil, errors.Wrap(err, "failed to get rune holders by rune id")
} }

View File

@@ -1,16 +0,0 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
)
func (u *Usecase) GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error) {
balances, err := u.runesDg.GetUnspentOutPointBalancesByPkScript(ctx, pkScript, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
return balances, nil
}

View File

@@ -8,8 +8,9 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/runes" "github.com/gaze-network/indexer-network/modules/runes/runes"
) )
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) { // Use limit = -1 as no limit.
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock) func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock, limit, offset)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error during GetTransactionsByHeight") return nil, errors.Wrap(err, "error during GetTransactionsByHeight")
} }

View File

@@ -0,0 +1,25 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
)
func (u *Usecase) GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
balances, err := u.runesDg.GetRunesUTXOsByPkScript(ctx, pkScript, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
return balances, nil
}
func (u *Usecase) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
balances, err := u.runesDg.GetRunesUTXOsByRuneIdAndPkScript(ctx, runeId, pkScript, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
return balances, nil
}