feat(runes): add Get Tokens API (#38)

* 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

* feat(runes): add get token list api

* fix(runes): use distinct to get token list

* feat: remove unused code

* fix: count holders distinct pkscript

* feat: implement additional scopes

* chore: comments

* feat: implement search

* refactor: switch to use paginationRequest

* refactor: rename get token list to get tokens

* fix: count total holders by rune ids

* fix: rename file

* fix: rename minting to ongoing

* fix: get ongoing check rune is mintable

* chore: disable gosec g115

* fix: pr

---------

Co-authored-by: Gaze <gazenw@users.noreply.github.com>
This commit is contained in:
gazenw
2024-10-06 19:30:57 +07:00
committed by GitHub
parent 1bd84b0154
commit 107836ae39
29 changed files with 665 additions and 47 deletions

View File

@@ -101,3 +101,6 @@ linters-settings:
attr-only: true
key-naming-case: snake
args-on-sep-lines: true
gosec:
excludes:
- G115

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: blocks.sql
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: events.sql
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: nodes.sql
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: nodesales.sql
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: test.sql
package gen

View File

@@ -11,11 +11,10 @@ import (
)
type getBalancesRequest struct {
paginationRequest
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
}
const (
@@ -66,8 +65,8 @@ func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if req.Limit == 0 {
req.Limit = getBalancesDefaultLimit
if err := req.ParseDefault(); err != nil {
return errors.WithStack(err)
}
pkScript, ok := resolvePkScript(h.network, req.Wallet)

View File

@@ -89,7 +89,7 @@ func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
}
if query.Limit == 0 {
query.Limit = getBalancesMaxLimit
query.Limit = getBalancesDefaultLimit
}
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight, query.Limit, query.Offset)

View File

@@ -15,15 +15,13 @@ import (
)
type getHoldersRequest struct {
paginationRequest
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
const (
getHoldersMaxLimit = 1000
getHoldersDefaultLimit = 100
getHoldersMaxLimit = 1000
)
func (r getHoldersRequest) Validate() error {
@@ -68,6 +66,9 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if err := req.ParseDefault(); err != nil {
return errors.WithStack(err)
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
@@ -78,10 +79,6 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
blockHeight = uint64(blockHeader.Height)
}
if req.Limit == 0 {
req.Limit = getHoldersDefaultLimit
}
var runeId runes.RuneId
if req.Id != "" {
var ok bool

View File

@@ -57,9 +57,9 @@ type getTokenInfoResult struct {
MintedAmount uint128.Uint128 `json:"mintedAmount"`
BurnedAmount uint128.Uint128 `json:"burnedAmount"`
Decimals uint8 `json:"decimals"`
DeployedAt uint64 `json:"deployedAt"` // unix timestamp
DeployedAt int64 `json:"deployedAt"` // unix timestamp
DeployedAtHeight uint64 `json:"deployedAtHeight"`
CompletedAt *uint64 `json:"completedAt"` // unix timestamp
CompletedAt *int64 `json:"completedAt"` // unix timestamp
CompletedAtHeight *uint64 `json:"completedAtHeight"`
HoldersCount int `json:"holdersCount"`
Extend tokenInfoExtend `json:"extend"`
@@ -144,9 +144,9 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
MintedAmount: mintedAmount,
BurnedAmount: runeEntry.BurnedAmount,
Decimals: runeEntry.Divisibility,
DeployedAt: uint64(runeEntry.EtchedAt.Unix()),
DeployedAt: runeEntry.EtchedAt.Unix(),
DeployedAtHeight: runeEntry.EtchingBlock,
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(uint64(runeEntry.CompletedAt.Unix()))),
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
CompletedAtHeight: runeEntry.CompletedAtHeight,
HoldersCount: len(holdingBalances),
Extend: tokenInfoExtend{

View File

@@ -0,0 +1,172 @@
package httphandler
import (
"fmt"
"strings"
"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"
)
const (
getTokensMaxLimit = 1000
)
type GetTokensScope string
const (
GetTokensScopeAll GetTokensScope = "all"
GetTokensScopeOngoing GetTokensScope = "ongoing"
)
func (s GetTokensScope) IsValid() bool {
switch s {
case GetTokensScopeAll, GetTokensScopeOngoing:
return true
}
return false
}
type getTokensRequest struct {
paginationRequest
Search string `query:"search"`
BlockHeight uint64 `query:"blockHeight"`
Scope GetTokensScope `query:"scope"`
}
func (req getTokensRequest) Validate() error {
var errList []error
if err := req.paginationRequest.Validate(); err != nil {
errList = append(errList, err)
}
if req.Limit > getTokensMaxLimit {
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
}
if req.Scope != "" && !req.Scope.IsValid() {
errList = append(errList, errors.Errorf("invalid scope: %s", req.Scope))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
func (req *getTokensRequest) ParseDefault() error {
if err := req.paginationRequest.ParseDefault(); err != nil {
return errors.WithStack(err)
}
if req.Scope == "" {
req.Scope = GetTokensScopeAll
}
return nil
}
type getTokensResult struct {
List []getTokenInfoResult `json:"list"`
}
type getTokensResponse = HttpResponse[getTokensResult]
func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
var req getTokensRequest
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if err := req.ParseDefault(); 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)
}
// remove spacers
search := strings.Replace(strings.Replace(req.Search, "•", "", -1), ".", "", -1)
var entries []*runes.RuneEntry
switch req.Scope {
case GetTokensScopeAll:
entries, err = h.usecase.GetRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryList")
}
case GetTokensScopeOngoing:
entries, err = h.usecase.GetOngoingRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryList")
}
default:
return errs.NewPublicError(fmt.Sprintf("invalid scope: %s", req.Scope))
}
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
totalHolders, err := h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
}
result := make([]getTokenInfoResult, 0, len(entries))
for _, ent := range entries {
totalSupply, err := ent.Supply()
if err != nil {
return errors.Wrap(err, "cannot get total supply of rune")
}
mintedAmount, err := ent.MintedAmount()
if err != nil {
return errors.Wrap(err, "cannot get minted amount of rune")
}
circulatingSupply := mintedAmount.Sub(ent.BurnedAmount)
terms := lo.FromPtr(ent.Terms)
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{
Result: &getTokensResult{
List: result,
},
}))
}

View File

@@ -16,17 +16,15 @@ import (
)
type getTransactionsRequest struct {
paginationRequest
Wallet string `query:"wallet"`
Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
ToBlock int64 `query:"toBlock"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
}
const (
getTransactionsMaxLimit = 3000
getTransactionsDefaultLimit = 100
getTransactionsMaxLimit = 3000
)
func (r getTransactionsRequest) Validate() error {
@@ -128,6 +126,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if err := req.ParseDefault(); err != nil {
return errors.WithStack(err)
}
var pkScript []byte
if req.Wallet != "" {
@@ -146,9 +147,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
}
}
if req.Limit == 0 {
req.Limit = getTransactionsDefaultLimit
}
// default to latest block
if req.ToBlock == 0 {

View File

@@ -12,16 +12,14 @@ import (
)
type getUTXOsRequest struct {
paginationRequest
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
}
const (
getUTXOsMaxLimit = 3000
getUTXOsDefaultLimit = 100
getUTXOsMaxLimit = 3000
)
func (r getUTXOsRequest) Validate() error {
@@ -78,16 +76,15 @@ func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if err := req.ParseDefault(); err != nil {
return errors.WithStack(err)
}
pkScript, ok := resolvePkScript(h.network, req.Wallet)
if !ok {
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
}
if req.Limit == 0 {
req.Limit = getUTXOsDefaultLimit
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())

View File

@@ -7,7 +7,9 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/indexer-network/modules/runes/usecase"
"github.com/gaze-network/indexer-network/pkg/logger"
@@ -31,6 +33,53 @@ type HttpResponse[T any] struct {
Result *T `json:"result,omitempty"`
}
type paginationRequest struct {
Offset int32 `query:"offset"`
Limit int32 `query:"limit"`
// OrderBy string `query:"orderBy"` // ASC or DESC
// SortBy string `query:"sortBy"` // column name
}
func (req paginationRequest) Validate() error {
var errList []error
// this just safeguard for limit,
// each path should have own validation.
if req.Limit > 10000 {
errList = append(errList, errors.Errorf("too large limit"))
}
if req.Limit < 0 {
errList = append(errList, errors.Errorf("limit must be greater than or equal to 0"))
}
if req.Offset < 0 {
errList = append(errList, errors.Errorf("offset must be greater than or equal to 0"))
}
// TODO:
// if req.OrderBy != "" && req.OrderBy != "ASC" && req.OrderBy != "DESC" {
// errList = append(errList, errors.Errorf("invalid orderBy value, must be `ASC` or `DESC`"))
// }
return errs.WithPublicMessage(errors.Join(errList...), "pagination validation error")
}
func (req *paginationRequest) ParseDefault() error {
if req == nil {
return nil
}
if req.Limit == 0 {
req.Limit = 100
}
// TODO:
// if req.OrderBy == "" {
// req.OrderBy = "ASC"
// }
return nil
}
func resolvePkScript(network common.Network, wallet string) ([]byte, bool) {
if wallet == "" {
return nil, false

View File

@@ -16,5 +16,6 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
r.Get("/utxos/output/:txHash", h.GetUTXOsOutputByLocation)
r.Get("/block", h.GetCurrentBlock)
r.Get("/tokens", h.GetTokens)
return nil
}

View File

@@ -1,5 +1,6 @@
BEGIN;
CREATE EXTENSION pg_trgm;
-- Indexer Client Information
CREATE TABLE IF NOT EXISTS "runes_indexer_stats" (
@@ -48,6 +49,7 @@ CREATE TABLE IF NOT EXISTS "runes_entries" (
"etched_at" TIMESTAMP NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_rune_idx ON "runes_entries" USING BTREE ("rune");
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_rune_gin_idx ON "runes_entries" USING GIN ("rune" gin_trgm_ops); -- to speed up queries with LIKE operator
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_number_idx ON "runes_entries" USING BTREE ("number");
CREATE TABLE IF NOT EXISTS "runes_entry_states" (

View File

@@ -13,6 +13,12 @@ SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3
-- 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;
-- name: GetTotalHoldersByRuneIds :many
WITH balances AS (
SELECT DISTINCT ON (rune_id, pkscript) * FROM runes_balances WHERE rune_id = ANY(@rune_ids::TEXT[]) AND block_height <= @block_height ORDER BY rune_id, pkscript, block_height DESC
)
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id;
-- name: GetOutPointBalancesAtOutPoint :many
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
@@ -57,6 +63,47 @@ SELECT * FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE runes_entries.rune_id = ANY(@rune_ids::text[]) AND etching_block <= @height;
-- name: GetRuneEntries :many
WITH states AS (
-- select latest state
SELECT DISTINCT ON (rune_id) * FROM runes_entry_states WHERE block_height <= @height ORDER BY rune_id, block_height DESC
)
SELECT * FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
@search = '' OR
runes_entries.rune ILIKE @search || '%'
)
ORDER BY runes_entries.number
LIMIT @_limit OFFSET @_offset;
-- name: GetOngoingRuneEntries :many
WITH states AS (
-- select latest state
SELECT DISTINCT ON (rune_id) * FROM runes_entry_states WHERE block_height <= @height::integer ORDER BY rune_id, block_height DESC
)
SELECT * FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
runes_entries.terms = TRUE AND
states.mints < runes_entries.terms_cap AND
(
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= @height::integer
) AND (
runes_entries.terms_height_end IS NULL OR @height::integer <= runes_entries.terms_height_end
) AND (
runes_entries.terms_offset_start IS NULL OR runes_entries.terms_offset_start + runes_entries.etching_block <= @height::integer
) AND (
runes_entries.terms_offset_end IS NULL OR @height::integer <= runes_entries.terms_offset_start + runes_entries.etching_block
)
) AND (
@search::text = '' OR
runes_entries.rune ILIKE @search::text || '%'
)
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
LIMIT @_limit OFFSET @_offset;
-- name: GetRuneIdFromRune :one
SELECT rune_id FROM runes_entries WHERE rune = $1;

View File

@@ -44,6 +44,10 @@ type RunesReaderDataGateway interface {
GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (*runes.RuneEntry, error)
// GetRuneEntryByRuneIdAndHeightBatch returns the RuneEntries for the given runeIds and block height.
GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error)
// GetRuneEntries returns a list of rune entries, sorted by etching order. If search is not empty, it will filter the results by rune name (prefix).
GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
// GetOngoingRuneEntries returns a list of ongoing rune entries (can still mint), sorted by mint progress percent. If search is not empty, it will filter the results by rune name (prefix).
GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
// CountRuneEntries returns the number of existing rune entries.
CountRuneEntries(ctx context.Context) (uint64, error)
@@ -56,6 +60,8 @@ type RunesReaderDataGateway interface {
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.
GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript []byte, runeId runes.RuneId, blockHeight uint64) (*entity.Balance, error)
// GetTotalHoldersByRuneIds returns the total holders of each the given runeIds.
GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error)
}
type RunesWriterDataGateway interface {

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: batch.go
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: data.sql
package gen
@@ -428,6 +428,118 @@ func (q *Queries) GetLatestIndexedBlock(ctx context.Context) (RunesIndexedBlock,
return i, err
}
const getOngoingRuneEntries = `-- name: GetOngoingRuneEntries :many
WITH states AS (
-- select latest state
SELECT DISTINCT ON (rune_id) rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entry_states WHERE block_height <= $1::integer ORDER BY rune_id, block_height DESC
)
SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, etching_block, etching_tx_hash, etched_at, states.rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
runes_entries.terms = TRUE AND
states.mints < runes_entries.terms_cap AND
(
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= $1::integer
) AND (
runes_entries.terms_height_end IS NULL OR $1::integer <= runes_entries.terms_height_end
) AND (
runes_entries.terms_offset_start IS NULL OR runes_entries.terms_offset_start + runes_entries.etching_block <= $1::integer
) AND (
runes_entries.terms_offset_end IS NULL OR $1::integer <= runes_entries.terms_offset_start + runes_entries.etching_block
)
) AND (
$2::text = '' OR
runes_entries.rune ILIKE $2::text || '%'
)
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
LIMIT $4 OFFSET $3
`
type GetOngoingRuneEntriesParams struct {
Height int32
Search string
Offset int32
Limit int32
}
type GetOngoingRuneEntriesRow struct {
RuneID string
Number int64
Rune string
Spacers int32
Premine pgtype.Numeric
Symbol int32
Divisibility int16
Terms bool
TermsAmount pgtype.Numeric
TermsCap pgtype.Numeric
TermsHeightStart pgtype.Int4
TermsHeightEnd pgtype.Int4
TermsOffsetStart pgtype.Int4
TermsOffsetEnd pgtype.Int4
Turbo bool
EtchingBlock int32
EtchingTxHash string
EtchedAt pgtype.Timestamp
RuneID_2 pgtype.Text
BlockHeight pgtype.Int4
Mints pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) GetOngoingRuneEntries(ctx context.Context, arg GetOngoingRuneEntriesParams) ([]GetOngoingRuneEntriesRow, error) {
rows, err := q.db.Query(ctx, getOngoingRuneEntries,
arg.Height,
arg.Search,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOngoingRuneEntriesRow
for rows.Next() {
var i GetOngoingRuneEntriesRow
if err := rows.Scan(
&i.RuneID,
&i.Number,
&i.Rune,
&i.Spacers,
&i.Premine,
&i.Symbol,
&i.Divisibility,
&i.Terms,
&i.TermsAmount,
&i.TermsCap,
&i.TermsHeightStart,
&i.TermsHeightEnd,
&i.TermsOffsetStart,
&i.TermsOffsetEnd,
&i.Turbo,
&i.EtchingBlock,
&i.EtchingTxHash,
&i.EtchedAt,
&i.RuneID_2,
&i.BlockHeight,
&i.Mints,
&i.BurnedAmount,
&i.CompletedAt,
&i.CompletedAtHeight,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOutPointBalancesAtOutPoint = `-- name: GetOutPointBalancesAtOutPoint :many
SELECT rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2
`
@@ -465,6 +577,105 @@ func (q *Queries) GetOutPointBalancesAtOutPoint(ctx context.Context, arg GetOutP
return items, nil
}
const getRuneEntries = `-- name: GetRuneEntries :many
WITH states AS (
-- select latest state
SELECT DISTINCT ON (rune_id) rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entry_states WHERE block_height <= $4 ORDER BY rune_id, block_height DESC
)
SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, etching_block, etching_tx_hash, etched_at, states.rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
$1 = '' OR
runes_entries.rune ILIKE $1 || '%'
)
ORDER BY runes_entries.number
LIMIT $3 OFFSET $2
`
type GetRuneEntriesParams struct {
Search interface{}
Offset int32
Limit int32
Height int32
}
type GetRuneEntriesRow struct {
RuneID string
Number int64
Rune string
Spacers int32
Premine pgtype.Numeric
Symbol int32
Divisibility int16
Terms bool
TermsAmount pgtype.Numeric
TermsCap pgtype.Numeric
TermsHeightStart pgtype.Int4
TermsHeightEnd pgtype.Int4
TermsOffsetStart pgtype.Int4
TermsOffsetEnd pgtype.Int4
Turbo bool
EtchingBlock int32
EtchingTxHash string
EtchedAt pgtype.Timestamp
RuneID_2 pgtype.Text
BlockHeight pgtype.Int4
Mints pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) GetRuneEntries(ctx context.Context, arg GetRuneEntriesParams) ([]GetRuneEntriesRow, error) {
rows, err := q.db.Query(ctx, getRuneEntries,
arg.Search,
arg.Offset,
arg.Limit,
arg.Height,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRuneEntriesRow
for rows.Next() {
var i GetRuneEntriesRow
if err := rows.Scan(
&i.RuneID,
&i.Number,
&i.Rune,
&i.Spacers,
&i.Premine,
&i.Symbol,
&i.Divisibility,
&i.Terms,
&i.TermsAmount,
&i.TermsCap,
&i.TermsHeightStart,
&i.TermsHeightEnd,
&i.TermsOffsetStart,
&i.TermsOffsetEnd,
&i.Turbo,
&i.EtchingBlock,
&i.EtchingTxHash,
&i.EtchedAt,
&i.RuneID_2,
&i.BlockHeight,
&i.Mints,
&i.BurnedAmount,
&i.CompletedAt,
&i.CompletedAtHeight,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRuneEntriesByRuneIds = `-- name: GetRuneEntriesByRuneIds :many
WITH states AS (
-- select latest state
@@ -971,6 +1182,43 @@ func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetR
return items, nil
}
const getTotalHoldersByRuneIds = `-- name: GetTotalHoldersByRuneIds :many
WITH balances AS (
SELECT DISTINCT ON (rune_id, pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = ANY($1::TEXT[]) AND block_height <= $2 ORDER BY rune_id, pkscript, block_height DESC
)
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id
`
type GetTotalHoldersByRuneIdsParams struct {
RuneIds []string
BlockHeight int32
}
type GetTotalHoldersByRuneIdsRow struct {
RuneID string
Count int64
}
func (q *Queries) GetTotalHoldersByRuneIds(ctx context.Context, arg GetTotalHoldersByRuneIdsParams) ([]GetTotalHoldersByRuneIdsRow, error) {
rows, err := q.db.Query(ctx, getTotalHoldersByRuneIds, arg.RuneIds, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTotalHoldersByRuneIdsRow
for rows.Next() {
var i GetTotalHoldersByRuneIdsRow
if err := rows.Scan(&i.RuneID, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const spendOutPointBalances = `-- name: SpendOutPointBalances :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
`

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: info.sql
package gen

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
package gen

View File

@@ -63,7 +63,7 @@ func mapIndexerStateTypeToParams(src entity.IndexerState) gen.SetIndexerStatePar
}
}
func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntry, error) {
func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {
return runes.RuneEntry{}, errors.Wrap(err, "failed to parse rune id")

View File

@@ -262,7 +262,7 @@ func (r *Repository) GetRuneEntryByRuneIdBatch(ctx context.Context, runeIds []ru
runeEntries := make(map[runes.RuneId]*runes.RuneEntry, len(rows))
var errs []error
for i, runeEntryModel := range rows {
runeEntry, err := mapRuneEntryModelToType(runeEntryModel)
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(runeEntryModel))
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
@@ -302,7 +302,7 @@ func (r *Repository) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, run
runeEntries := make(map[runes.RuneId]*runes.RuneEntry, len(rows))
var errs []error
for i, runeEntryModel := range rows {
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesByRuneIdsRow(runeEntryModel))
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(runeEntryModel))
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
@@ -316,6 +316,62 @@ func (r *Repository) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, run
return runeEntries, nil
}
func (r *Repository) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error) {
rows, err := r.queries.GetRuneEntries(ctx, gen.GetRuneEntriesParams{
Search: search,
Height: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
runeEntries := make([]*runes.RuneEntry, 0, len(rows))
var errs []error
for i, model := range rows {
runeEntry, err := mapRuneEntryModelToType(model)
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
}
runeEntries = append(runeEntries, &runeEntry)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return runeEntries, nil
}
func (r *Repository) GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error) {
rows, err := r.queries.GetOngoingRuneEntries(ctx, gen.GetOngoingRuneEntriesParams{
Search: search,
Height: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
runeEntries := make([]*runes.RuneEntry, 0, len(rows))
var errs []error
for i, model := range rows {
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(model))
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
}
runeEntries = append(runeEntries, &runeEntry)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return runeEntries, nil
}
func (r *Repository) CountRuneEntries(ctx context.Context) (uint64, error) {
count, err := r.queries.CountRuneEntries(ctx)
if err != nil {
@@ -400,6 +456,25 @@ func (r *Repository) GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript
return result, nil
}
func (r *Repository) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error) {
rows, err := r.queries.GetTotalHoldersByRuneIds(ctx, gen.GetTotalHoldersByRuneIdsParams{
RuneIds: lo.Map(runeIds, func(runeId runes.RuneId, _ int) string { return runeId.String() }),
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
holders := make(map[runes.RuneId]int64, len(rows))
for _, row := range rows {
runeId, err := runes.NewRuneIdFromString(row.RuneID)
if err != nil {
return nil, errors.Wrap(err, "failed to parse RuneId")
}
holders[runeId] = row.Count
}
return holders, nil
}
func (r *Repository) CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error {
if tx == nil {
return nil

View File

@@ -25,3 +25,11 @@ func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId,
}
return balances, nil
}
func (u *Usecase) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error) {
holders, err := u.runesDg.GetTotalHoldersByRuneIds(ctx, runeIds, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "failed to get total holders by rune ids")
}
return holders, nil
}

View File

@@ -46,3 +46,19 @@ func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeId
}
return runeEntry, nil
}
func (u *Usecase) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
entries, err := u.runesDg.GetRuneEntries(ctx, search, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to listing rune entries")
}
return entries, nil
}
func (u *Usecase) GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
entries, err := u.runesDg.GetOngoingRuneEntries(ctx, search, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to listing rune entries")
}
return entries, nil
}