feat(brc20): implement brc-20 indexer api

This commit is contained in:
Gaze
2024-06-10 20:12:40 +07:00
parent 84bbc986f0
commit 2c9adb7e91
25 changed files with 2116 additions and 5 deletions

11
modules/brc20/api/api.go Normal file
View File

@@ -0,0 +1,11 @@
package api
import (
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/modules/runes/api/httphandler"
"github.com/gaze-network/indexer-network/modules/runes/usecase"
)
func NewHTTPHandler(network common.Network, usecase *usecase.Usecase) *httphandler.HttpHandler {
return httphandler.New(network, usecase)
}

View File

@@ -0,0 +1,115 @@
package httphandler
import (
"slices"
"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/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/holiman/uint256"
"github.com/samber/lo"
)
type getBalancesByAddressRequest struct {
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r getBalancesByAddressRequest) Validate() error {
var errList []error
if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type balanceExtend struct {
Transferable *uint256.Int `json:"transferable"`
Available *uint256.Int `json:"available"`
}
type balance struct {
Amount *uint256.Int `json:"amount"`
Id string `json:"id"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals uint16 `json:"decimals"`
Extend balanceExtend `json:"extend"`
}
type getBalancesByAddressResult struct {
List []balance `json:"list"`
BlockHeight uint64 `json:"blockHeight"`
}
type getBalancesByAddressResponse = common.HttpResponse[getBalancesByAddressResult]
func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
pkScript, err := btcutils.ToPkScript(h.network, req.Wallet)
if err != nil {
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
balanceRuneIds := lo.Keys(balances)
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), balanceRuneIds)
if err != nil {
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
}
balanceList := make([]balance, 0, len(balances))
for id, b := range balances {
entry := entries[id]
balanceList = append(balanceList, balance{
Amount: decimals.ToUint256(b.OverallBalance, entry.Decimals),
Id: id,
Name: entry.OriginalTick,
Symbol: entry.Tick,
Decimals: entry.Decimals,
Extend: balanceExtend{
Transferable: decimals.ToUint256(b.OverallBalance.Sub(b.AvailableBalance), entry.Decimals),
Available: decimals.ToUint256(b.AvailableBalance, entry.Decimals),
},
})
}
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
resp := getBalancesByAddressResponse{
Result: &getBalancesByAddressResult{
BlockHeight: blockHeight,
List: balanceList,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,125 @@
package httphandler
import (
"context"
"slices"
"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/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
type getBalancesByAddressBatchRequest struct {
Queries []getBalancesByAddressRequest `json:"queries"`
}
func (r getBalancesByAddressBatchRequest) Validate() error {
var errList []error
for _, query := range r.Queries {
if query.Wallet == "" {
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required"))
}
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getBalancesByAddressBatchResult struct {
List []*getBalancesByAddressResult `json:"list"`
}
type getBalancesByAddressBatchResponse = common.HttpResponse[getBalancesByAddressBatchResult]
func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressBatchRequest
if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
var latestBlockHeight uint64
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
latestBlockHeight = uint64(blockHeader.Height)
processQuery := func(ctx context.Context, query getBalancesByAddressRequest) (*getBalancesByAddressResult, error) {
pkScript, err := btcutils.ToPkScript(h.network, query.Wallet)
if err != nil {
return nil, errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
}
blockHeight := query.BlockHeight
if blockHeight == 0 {
blockHeight = latestBlockHeight
}
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
balanceRuneIds := lo.Keys(balances)
entries, err := h.usecase.GetTickEntryByTickBatch(ctx, balanceRuneIds)
if err != nil {
return nil, errors.Wrap(err, "error during GetTickEntryByTickBatch")
}
balanceList := make([]balance, 0, len(balances))
for id, b := range balances {
entry := entries[id]
balanceList = append(balanceList, balance{
Amount: decimals.ToUint256(b.OverallBalance, entry.Decimals),
Id: id,
Name: entry.OriginalTick,
Symbol: entry.Tick,
Decimals: entry.Decimals,
Extend: balanceExtend{
Transferable: decimals.ToUint256(b.OverallBalance.Sub(b.AvailableBalance), entry.Decimals),
Available: decimals.ToUint256(b.AvailableBalance, entry.Decimals),
},
})
}
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
return &getBalancesByAddressResult{
BlockHeight: blockHeight,
List: balanceList,
}, nil
}
results := make([]*getBalancesByAddressResult, len(req.Queries))
eg, ectx := errgroup.WithContext(ctx.UserContext())
for i, query := range req.Queries {
i := i
query := query
eg.Go(func() error {
result, err := processQuery(ectx, query)
if err != nil {
return errors.Wrapf(err, "error during processQuery for query %d", i)
}
results[i] = result
return nil
})
}
if err := eg.Wait(); err != nil {
return errors.WithStack(err)
}
resp := getBalancesByAddressBatchResponse{
Result: &getBalancesByAddressBatchResult{
List: results,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,49 @@
package httphandler
import (
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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/core/types"
"github.com/gofiber/fiber/v2"
)
// TODO: use modules/brc20/constants.go
var startingBlockHeader = map[common.Network]types.BlockHeader{
common.NetworkMainnet: {
Height: 767429,
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000000000002b35aef66eb15cd2b232a800f75a2f25cedca4cfe52c4")),
},
common.NetworkTestnet: {
Height: 2413342,
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000022e97030b143af785de812f836dd0651b6ac2b7dd9e90dc9abf9")),
},
}
type getCurrentBlockResult struct {
Hash string `json:"hash"`
Height int64 `json:"height"`
}
type getCurrentBlockResponse = common.HttpResponse[getCurrentBlockResult]
func (h *HttpHandler) GetCurrentBlock(ctx *fiber.Ctx) (err error) {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "error during get latest block")
}
blockHeader = startingBlockHeader[h.network]
}
resp := getCurrentBlockResponse{
Result: &getCurrentBlockResult{
Hash: blockHeader.Hash.String(),
Height: blockHeader.Height,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,107 @@
package httphandler
import (
"encoding/hex"
"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/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/holiman/uint256"
)
type getHoldersRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r getHoldersRequest) Validate() error {
var errList []error
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type holdingBalanceExtend struct {
Transferable *uint256.Int `json:"transferable"`
Available *uint256.Int `json:"available"`
}
type holdingBalance struct {
Address string `json:"address"`
PkScript string `json:"pkScript"`
Amount *uint256.Int `json:"amount"`
Percent float64 `json:"percent"`
Extend holdingBalanceExtend `json:"extend"`
}
type getHoldersResult struct {
BlockHeight uint64 `json:"blockHeight"`
TotalSupply *uint256.Int `json:"totalSupply"`
MintedAmount *uint256.Int `json:"mintedAmount"`
Decimals uint16 `json:"decimals"`
List []holdingBalance `json:"list"`
}
type getHoldersResponse = common.HttpResponse[getHoldersResult]
func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
var req getHoldersRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
entry, err := h.usecase.GetTickEntryByTickAndHeight(ctx.UserContext(), req.Id, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTickEntryByTickAndHeight")
}
holdingBalances, err := h.usecase.GetBalancesByTick(ctx.UserContext(), req.Id, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByTick")
}
list := make([]holdingBalance, 0, len(holdingBalances))
for _, balance := range holdingBalances {
address, err := btcutils.PkScriptToAddress(balance.PkScript, h.network)
if err != nil {
return errors.Wrapf(err, "can't convert pkscript(%x) to address", balance.PkScript)
}
percent := balance.OverallBalance.Div(entry.TotalSupply)
list = append(list, holdingBalance{
Address: address,
PkScript: hex.EncodeToString(balance.PkScript),
Amount: decimals.ToUint256(balance.OverallBalance, entry.Decimals),
Percent: percent.InexactFloat64(),
Extend: holdingBalanceExtend{
Transferable: decimals.ToUint256(balance.OverallBalance.Sub(balance.AvailableBalance), entry.Decimals),
Available: decimals.ToUint256(balance.AvailableBalance, entry.Decimals),
},
})
}
resp := getHoldersResponse{
Result: &getHoldersResult{
BlockHeight: blockHeight,
TotalSupply: decimals.ToUint256(entry.TotalSupply, entry.Decimals), // TODO: convert to wei
MintedAmount: decimals.ToUint256(entry.MintedAmount, entry.Decimals), // TODO: convert to wei
List: list,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,149 @@
package httphandler
import (
"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/brc20/internal/entity"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/holiman/uint256"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
type getTokenInfoRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r getTokenInfoRequest) Validate() error {
var errList []error
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type tokenInfoExtend struct {
DeployedBy string `json:"deployedBy"`
LimitPerMint *uint256.Int `json:"limitPerMint"`
DeployInscriptionId string `json:"deployInscriptionId"`
DeployInscriptionNumber int64 `json:"deployInscriptionNumber"`
InscriptionStartNumber int64 `json:"inscriptionStartNumber"`
InscriptionEndNumber int64 `json:"inscriptionEndNumber"`
}
type getTokenInfoResult struct {
Id string `json:"id"`
Name string `json:"name"`
Symbol string `json:"symbol"`
TotalSupply *uint256.Int `json:"totalSupply"`
CirculatingSupply *uint256.Int `json:"circulatingSupply"`
MintedAmount *uint256.Int `json:"mintedAmount"`
BurnedAmount *uint256.Int `json:"burnedAmount"`
Decimals uint16 `json:"decimals"`
DeployedAt uint64 `json:"deployedAt"`
DeployedAtHeight uint64 `json:"deployedAtHeight"`
CompletedAt *uint64 `json:"completedAt"`
CompletedAtHeight *uint64 `json:"completedAtHeight"`
HoldersCount int `json:"holdersCount"`
Extend tokenInfoExtend `json:"extend"`
}
type getTokenInfoResponse = common.HttpResponse[getTokenInfoResult]
func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
var req getTokenInfoRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
group, groupctx := errgroup.WithContext(ctx.UserContext())
var (
entry *entity.TickEntry
firstInscriptionNumber, lastInscriptionNumber int64
deployEvent *entity.EventDeploy
holdingBalances []*entity.Balance
)
group.Go(func() error {
deployEvent, err = h.usecase.GetDeployEventByTick(groupctx, req.Id)
if err != nil {
return errors.Wrap(err, "error during GetDeployEventByTick")
}
return nil
})
group.Go(func() error {
firstInscriptionNumber, lastInscriptionNumber, err = h.usecase.GetFirstLastInscriptionNumberByTick(groupctx, req.Id)
if err != nil {
return errors.Wrap(err, "error during GetFirstLastInscriptionNumberByTick")
}
return nil
})
group.Go(func() error {
entry, err = h.usecase.GetTickEntryByTickAndHeight(groupctx, req.Id, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTickEntryByTickAndHeight")
}
return nil
})
group.Go(func() error {
balances, err := h.usecase.GetBalancesByTick(groupctx, req.Id, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
holdingBalances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
return !b.OverallBalance.IsZero()
})
return nil
})
if err := group.Wait(); err != nil {
return errors.WithStack(err)
}
address, err := btcutils.PkScriptToAddress(deployEvent.PkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for pkscript: %x, network: %v`, deployEvent.PkScript, h.network)
}
resp := getTokenInfoResponse{
Result: &getTokenInfoResult{
Id: entry.Tick,
Name: entry.OriginalTick,
Symbol: entry.Tick,
TotalSupply: decimals.ToUint256(entry.TotalSupply, entry.Decimals),
CirculatingSupply: decimals.ToUint256(entry.MintedAmount.Sub(entry.BurnedAmount), entry.Decimals),
MintedAmount: decimals.ToUint256(entry.MintedAmount, entry.Decimals),
BurnedAmount: decimals.ToUint256(entry.BurnedAmount, entry.Decimals),
Decimals: entry.Decimals,
DeployedAt: uint64(entry.DeployedAt.Unix()),
DeployedAtHeight: entry.DeployedAtHeight,
CompletedAt: lo.Ternary(entry.CompletedAt.IsZero(), nil, lo.ToPtr(uint64(entry.CompletedAt.Unix()))),
CompletedAtHeight: lo.Ternary(entry.CompletedAtHeight == 0, nil, lo.ToPtr(entry.CompletedAtHeight)),
HoldersCount: len(holdingBalances),
Extend: tokenInfoExtend{
DeployedBy: address,
LimitPerMint: decimals.ToUint256(entry.LimitPerMint, entry.Decimals),
DeployInscriptionId: deployEvent.InscriptionId.String(),
DeployInscriptionNumber: deployEvent.InscriptionNumber,
InscriptionStartNumber: lo.Ternary(firstInscriptionNumber < 0, deployEvent.InscriptionNumber, firstInscriptionNumber),
InscriptionEndNumber: lo.Ternary(lastInscriptionNumber < 0, deployEvent.InscriptionNumber, lastInscriptionNumber),
},
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,465 @@
package httphandler
import (
"bytes"
"cmp"
"encoding/hex"
"slices"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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/brc20/internal/entity"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/holiman/uint256"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"golang.org/x/sync/errgroup"
)
var ops = []string{"inscribe-deploy", "inscribe-mint", "inscribe-transfer", "transfer-transfer"}
type getTransactionsRequest struct {
Wallet string `query:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
Op string `query:"op"`
}
func (r getTransactionsRequest) Validate() error {
var errList []error
if r.Op != "" {
if !lo.Contains(ops, r.Op) {
errList = append(errList, errors.Errorf("invalid 'op' value: %s, supported values: %s", r.Op, strings.Join(ops, ", ")))
}
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type txOpDeployArg struct {
Op string `json:"op"`
Tick string `json:"tick"`
Max decimal.Decimal `json:"max"`
Lim decimal.Decimal `json:"lim"`
Dec uint16 `json:"dec"`
SelfMint bool `json:"self_mint"`
}
type txOpGeneralArg struct {
Op string `json:"op"`
Tick string `json:"tick"`
Amount decimal.Decimal `json:"amt"`
}
type txOperation[T any] struct {
InscriptionId string `json:"inscriptionId"`
InscriptionNumber int64 `json:"inscriptionNumber"`
Op string `json:"op"`
Args T `json:"args"`
}
type txOperationsDeploy struct {
txOperation[txOpDeployArg]
Address string `json:"address"`
}
type txOperationsMint struct {
txOperation[txOpGeneralArg]
Address string `json:"address"`
}
type txOperationsInscribeTransfer struct {
txOperation[txOpGeneralArg]
Address string `json:"address"`
OutputIndex uint32 `json:"outputIndex"`
Sats uint64 `json:"sats"`
}
type txOperationsTransferTransfer struct {
txOperation[txOpGeneralArg]
FromAddress string `json:"fromAddress"`
ToAddress string `json:"toAddress"`
}
type transactionExtend struct {
Operations []any `json:"operations"`
}
type amountWithDecimal struct {
Amount *uint256.Int `json:"amount"`
Decimals uint16 `json:"decimals"`
}
type txInputOutput struct {
PkScript string `json:"pkScript"`
Address string `json:"address"`
Id string `json:"id"`
Amount *uint256.Int `json:"amount"`
Decimals uint16 `json:"decimals"`
Index uint32 `json:"index"`
}
type transaction struct {
TxHash chainhash.Hash `json:"txHash"`
BlockHeight uint64 `json:"blockHeight"`
Index uint32 `json:"index"`
Timestamp int64 `json:"timestamp"`
Inputs []txInputOutput `json:"inputs"`
Outputs []txInputOutput `json:"outputs"`
Mints map[string]amountWithDecimal `json:"mints"`
Burns map[string]amountWithDecimal `json:"burns"`
Extend transactionExtend `json:"extend"`
}
type getTransactionsResult struct {
List []transaction `json:"list"`
}
type getTransactionsResponse = common.HttpResponse[getTransactionsResult]
func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
var req getTransactionsRequest
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
var pkScript []byte
if req.Wallet != "" {
pkScript, err = btcutils.ToPkScript(h.network, req.Wallet)
if err != nil {
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
}
}
blockHeight := req.BlockHeight
// set blockHeight to the latest block height blockHeight, pkScript, and runeId are not provided
if blockHeight == 0 && pkScript == nil && req.Id == "" {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
var (
deployEvents []*entity.EventDeploy
mintEvents []*entity.EventMint
transferTransferEvents []*entity.EventTransferTransfer
inscribeTransferEvents []*entity.EventInscribeTransfer
)
group, groupctx := errgroup.WithContext(ctx.UserContext())
if req.Op == "" || req.Op == "inscribe-deploy" {
group.Go(func() error {
events, err := h.usecase.GetDeployEvents(groupctx, pkScript, req.Id, blockHeight)
deployEvents = events
return errors.Wrap(err, "error during get inscribe-deploy events")
})
}
if req.Op == "" || req.Op == "inscribe-mint" {
group.Go(func() error {
events, err := h.usecase.GetMintEvents(groupctx, pkScript, req.Id, blockHeight)
mintEvents = events
return errors.Wrap(err, "error during get inscribe-mint events")
})
}
if req.Op == "" || req.Op == "transfer-transfer" {
group.Go(func() error {
events, err := h.usecase.GetTransferTransferEvents(groupctx, pkScript, req.Id, blockHeight)
transferTransferEvents = events
return errors.Wrap(err, "error during get transfer-transfer events")
})
}
if req.Op == "" || req.Op == "inscribe-transfer" {
group.Go(func() error {
events, err := h.usecase.GetInscribeTransferEvents(groupctx, pkScript, req.Id, blockHeight)
inscribeTransferEvents = events
return errors.Wrap(err, "error during get inscribe-transfer events")
})
}
if err := group.Wait(); err != nil {
return errors.WithStack(err)
}
allTicks := make([]string, 0, len(deployEvents)+len(mintEvents)+len(transferTransferEvents)+len(inscribeTransferEvents))
allTicks = append(allTicks, lo.Map(deployEvents, func(event *entity.EventDeploy, _ int) string { return event.Tick })...)
allTicks = append(allTicks, lo.Map(mintEvents, func(event *entity.EventMint, _ int) string { return event.Tick })...)
allTicks = append(allTicks, lo.Map(transferTransferEvents, func(event *entity.EventTransferTransfer, _ int) string { return event.Tick })...)
allTicks = append(allTicks, lo.Map(inscribeTransferEvents, func(event *entity.EventInscribeTransfer, _ int) string { return event.Tick })...)
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), lo.Uniq(allTicks))
if err != nil {
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
}
rawTxList := make([]transaction, 0, len(deployEvents)+len(mintEvents)+len(transferTransferEvents)+len(inscribeTransferEvents))
// Deploy events
for _, event := range deployEvents {
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
}
respTx := transaction{
TxHash: event.TxHash,
BlockHeight: event.BlockHeight,
Index: event.TxIndex,
Timestamp: event.Timestamp.Unix(),
Mints: map[string]amountWithDecimal{},
Burns: map[string]amountWithDecimal{},
Extend: transactionExtend{
Operations: []any{
txOperationsDeploy{
txOperation: txOperation[txOpDeployArg]{
InscriptionId: event.InscriptionId.String(),
InscriptionNumber: event.InscriptionNumber,
Op: "deploy",
Args: txOpDeployArg{
Op: "deploy",
Tick: event.Tick,
Max: event.TotalSupply,
Lim: event.LimitPerMint,
Dec: event.Decimals,
SelfMint: event.IsSelfMint,
},
},
Address: address,
},
},
},
}
rawTxList = append(rawTxList, respTx)
}
// Mint events
for _, event := range mintEvents {
entry := entries[event.Tick]
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
}
amtWei := decimals.ToUint256(event.Amount, entry.Decimals)
respTx := transaction{
TxHash: event.TxHash,
BlockHeight: event.BlockHeight,
Index: event.TxIndex,
Timestamp: event.Timestamp.Unix(),
Outputs: []txInputOutput{
{
PkScript: hex.EncodeToString(event.PkScript),
Address: address,
Id: event.Tick,
Amount: amtWei,
Decimals: entry.Decimals,
Index: event.TxIndex,
},
},
Mints: map[string]amountWithDecimal{
event.Tick: {
Amount: amtWei,
Decimals: entry.Decimals,
},
},
Extend: transactionExtend{
Operations: []any{
txOperationsMint{
txOperation: txOperation[txOpGeneralArg]{
InscriptionId: event.InscriptionId.String(),
InscriptionNumber: event.InscriptionNumber,
Op: "inscribe-mint",
Args: txOpGeneralArg{
Op: "inscribe-mint",
Tick: event.Tick,
Amount: event.Amount,
},
},
Address: address,
},
},
},
}
rawTxList = append(rawTxList, respTx)
}
// Inscribe Transfer events
for _, event := range inscribeTransferEvents {
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
}
respTx := transaction{
TxHash: event.TxHash,
BlockHeight: event.BlockHeight,
Index: event.TxIndex,
Timestamp: event.Timestamp.Unix(),
Mints: map[string]amountWithDecimal{},
Burns: map[string]amountWithDecimal{},
Extend: transactionExtend{
Operations: []any{
txOperationsInscribeTransfer{
txOperation: txOperation[txOpGeneralArg]{
InscriptionId: event.InscriptionId.String(),
InscriptionNumber: event.InscriptionNumber,
Op: "inscribe-transfer",
Args: txOpGeneralArg{
Op: "inscribe-transfer",
Tick: event.Tick,
Amount: event.Amount,
},
},
Address: address,
OutputIndex: event.SatPoint.OutPoint.Index,
Sats: event.SatsAmount,
},
},
},
}
rawTxList = append(rawTxList, respTx)
}
// Transfer Transfer events
for _, event := range transferTransferEvents {
entry := entries[event.Tick]
amntWei := decimals.ToUint256(event.Amount, entry.Decimals)
fromAddress, err := btcutils.PkScriptToAddress(event.FromPkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.FromPkScript, h.network)
}
toAddress := ""
if len(event.ToPkScript) > 0 && !bytes.Equal(event.ToPkScript, []byte{0x6a}) {
toAddress, err = btcutils.PkScriptToAddress(event.ToPkScript, h.network)
if err != nil {
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.FromPkScript, h.network)
}
}
// if toAddress is empty, it's a burn.
burns := map[string]amountWithDecimal{}
if len(toAddress) == 0 {
burns[event.Tick] = amountWithDecimal{
Amount: amntWei,
Decimals: entry.Decimals,
}
}
respTx := transaction{
TxHash: event.TxHash,
BlockHeight: event.BlockHeight,
Index: event.TxIndex,
Timestamp: event.Timestamp.Unix(),
Inputs: []txInputOutput{
{
PkScript: hex.EncodeToString(event.FromPkScript),
Address: fromAddress,
Id: event.Tick,
Amount: amntWei,
Decimals: entry.Decimals,
Index: event.ToOutputIndex,
},
},
Outputs: []txInputOutput{
{
PkScript: hex.EncodeToString(event.ToPkScript),
Address: fromAddress,
Id: event.Tick,
Amount: amntWei,
Decimals: entry.Decimals,
Index: event.ToOutputIndex,
},
},
Mints: map[string]amountWithDecimal{},
Burns: burns,
Extend: transactionExtend{
Operations: []any{
txOperationsTransferTransfer{
txOperation: txOperation[txOpGeneralArg]{
InscriptionId: event.InscriptionId.String(),
InscriptionNumber: event.InscriptionNumber,
Op: "transfer-transfer",
Args: txOpGeneralArg{
Op: "transfer-transfer",
Tick: event.Tick,
Amount: event.Amount,
},
},
FromAddress: fromAddress,
ToAddress: toAddress,
},
},
},
}
rawTxList = append(rawTxList, respTx)
}
// merge brc-20 tx events that have the same tx hash
txList := make([]transaction, 0, len(rawTxList))
groupedTxs := lo.GroupBy(rawTxList, func(tx transaction) chainhash.Hash { return tx.TxHash })
for _, txs := range groupedTxs {
tx := txs[0]
if tx.Mints == nil {
tx.Mints = map[string]amountWithDecimal{}
}
if tx.Burns == nil {
tx.Burns = map[string]amountWithDecimal{}
}
for _, tx2 := range txs[1:] {
tx.Inputs = append(tx.Inputs, tx2.Inputs...)
tx.Outputs = append(tx.Outputs, tx2.Outputs...)
for tick, tx2Ammt := range tx2.Mints {
// merge the amount if same tick
// TODO: or it shouldn't happen?
if txAmmt, ok := tx.Mints[tick]; ok {
tx.Mints[tick] = amountWithDecimal{
Amount: new(uint256.Int).Add(txAmmt.Amount, tx2Ammt.Amount),
Decimals: txAmmt.Decimals,
}
} else {
tx.Mints[tick] = tx2Ammt
}
}
for tick, tx2Ammt := range tx2.Burns {
// merge the amount if same tick
// TODO: or it shouldn't happen?
if txAmmt, ok := tx.Burns[tick]; ok {
tx.Burns[tick] = amountWithDecimal{
Amount: new(uint256.Int).Add(txAmmt.Amount, tx2Ammt.Amount),
Decimals: txAmmt.Decimals,
}
} else {
tx.Burns[tick] = tx2Ammt
}
}
tx.Extend.Operations = append(tx.Extend.Operations, tx2.Extend.Operations...)
}
slices.SortFunc(tx.Inputs, func(i, j txInputOutput) int {
return cmp.Compare(i.Index, j.Index)
})
slices.SortFunc(tx.Outputs, func(i, j txInputOutput) int {
return cmp.Compare(i.Index, j.Index)
})
txList = append(txList, tx)
}
// sort by block height ASC, then index ASC
slices.SortFunc(txList, func(t1, t2 transaction) int {
if t1.BlockHeight != t2.BlockHeight {
return int(t1.BlockHeight - t2.BlockHeight)
}
return int(t1.Index - t2.Index)
})
resp := getTransactionsResponse{
Result: &getTransactionsResult{
List: txList,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,135 @@
package httphandler
import (
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"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/brc20/internal/entity"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/gaze-network/indexer-network/pkg/decimals"
"github.com/gofiber/fiber/v2"
"github.com/holiman/uint256"
"github.com/samber/lo"
)
type getUTXOsByAddressRequest struct {
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r getUTXOsByAddressRequest) Validate() error {
var errList []error
if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type transferableInscription struct {
Ticker string `json:"ticker"`
Amount *uint256.Int `json:"amount"`
Decimals uint16 `json:"decimals"`
}
type utxoExtend struct {
TransferableInscriptions []transferableInscription `json:"transferableInscriptions"`
}
type utxo struct {
TxHash chainhash.Hash `json:"txHash"`
OutputIndex uint32 `json:"outputIndex"`
Extend utxoExtend `json:"extend"`
}
type getUTXOsByAddressResult struct {
List []utxo `json:"list"`
BlockHeight uint64 `json:"blockHeight"`
}
type getUTXOsByAddressResponse = common.HttpResponse[getUTXOsByAddressResult]
func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) {
var req getUTXOsByAddressRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
pkScript, err := btcutils.ToPkScript(h.network, req.Wallet)
if err != nil {
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
transferables, err := h.usecase.GetTransferableTransfersByPkScript(ctx.UserContext(), pkScript, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTransferableTransfersByPkScript")
}
transferableTicks := lo.Map(transferables, func(src *entity.EventInscribeTransfer, _ int) string { return src.Tick })
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), transferableTicks)
if err != nil {
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
}
groupedtransferableTi := lo.GroupBy(transferables, func(src *entity.EventInscribeTransfer) wire.OutPoint { return src.SatPoint.OutPoint })
utxoList := make([]utxo, 0, len(groupedtransferableTi))
for outPoint, transferables := range groupedtransferableTi {
transferableInscriptions := make([]transferableInscription, 0, len(transferables))
for _, transferable := range transferables {
entry := entries[transferable.Tick]
transferableInscriptions = append(transferableInscriptions, transferableInscription{
Ticker: transferable.Tick,
Amount: decimals.ToUint256(transferable.Amount, entry.Decimals),
Decimals: entry.Decimals,
})
}
utxoList = append(utxoList, utxo{
TxHash: outPoint.Hash,
OutputIndex: outPoint.Index,
Extend: utxoExtend{
TransferableInscriptions: transferableInscriptions,
},
})
}
// filter by req.Id if exists
{
utxoList = lo.Filter(utxoList, func(u utxo, _ int) bool {
for _, transferableInscriptions := range u.Extend.TransferableInscriptions {
if ok := strings.EqualFold(req.Id, transferableInscriptions.Ticker); ok {
return ok
}
}
return false
})
}
resp := getUTXOsByAddressResponse{
Result: &getUTXOsByAddressResult{
BlockHeight: blockHeight,
List: utxoList,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,18 @@
package httphandler
import (
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/modules/brc20/internal/usecase"
)
type HttpHandler struct {
usecase *usecase.Usecase
network common.Network
}
func New(network common.Network, usecase *usecase.Usecase) *HttpHandler {
return &HttpHandler{
network: network,
usecase: usecase,
}
}

View File

@@ -0,0 +1,19 @@
package httphandler
import (
"github.com/gofiber/fiber/v2"
)
func (h *HttpHandler) Mount(router fiber.Router) error {
r := router.Group("/v2/brc20")
r.Post("/balances/wallet/batch", h.GetBalancesByAddressBatch)
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress)
r.Get("/transactions", h.GetTransactions)
r.Get("/holders/:id", h.GetHolders)
r.Get("/info/:id", h.GetTokenInfo)
r.Get("/utxos/wallet/:wallet", h.GetUTXOsByAddress)
r.Get("/block", h.GetCurrentBlock)
return nil
}

View File

@@ -134,7 +134,7 @@ CREATE TABLE IF NOT EXISTS "brc20_balances" (
"pkscript" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tick" TEXT NOT NULL,
"overall_balance" DECIMAL NOT NULL,
"overall_balance" DECIMAL NOT NULL, -- overall balance = available_balance + transferable_balance
"available_balance" DECIMAL NOT NULL,
PRIMARY KEY ("pkscript", "tick", "block_height")
);

View File

@@ -28,8 +28,8 @@ SELECT * FROM brc20_event_deploys WHERE tick = $1;
-- name: GetFirstLastInscriptionNumberByTick :one
SELECT
COALESCE(MIN("inscription_number"), -1) AS "first_inscription_number",
COALESCE(MAX("inscription_number"), -1) AS "last_inscription_number"
COALESCE(MIN("inscription_number"), -1)::BIGINT AS "first_inscription_number",
COALESCE(MAX("inscription_number"), -1)::BIGINT AS "last_inscription_number"
FROM (
SELECT inscription_number FROM "brc20_event_mints" WHERE "brc20_event_mints"."tick" = $1
UNION ALL

View File

@@ -34,6 +34,15 @@ type BRC20ReaderDataGateway interface {
GetTickEntriesByTicks(ctx context.Context, ticks []string) (map[string]*entity.TickEntry, error)
GetEventInscribeTransfersByInscriptionIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*entity.EventInscribeTransfer, error)
GetLatestEventId(ctx context.Context) (int64, error)
GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error)
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error)
GetTransferableTransfersByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.EventInscribeTransfer, error)
GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error)
GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (first, last int64, err error)
GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error)
GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error)
GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error)
GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error)
}
type BRC20WriterDataGateway interface {

View File

@@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/hex"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@@ -231,6 +232,179 @@ func (r *Repository) GetTickEntriesByTicks(ctx context.Context, ticks []string)
return result, nil
}
func (r *Repository) GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error) {
models, err := r.queries.GetBalancesByTick(ctx, gen.GetBalancesByTickParams{
Tick: tick,
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.Balance, 0, len(models))
for _, model := range models {
balance, err := mapBalanceModelToType(gen.Brc20Balance(model))
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model")
}
result = append(result, &balance)
}
return result, nil
}
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error) {
models, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[string]*entity.Balance)
for _, model := range models {
balance, err := mapBalanceModelToType(gen.Brc20Balance(model))
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model")
}
result[balance.Tick] = &balance
}
return result, nil
}
func (r *Repository) GetTransferableTransfersByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.EventInscribeTransfer, error) {
models, err := r.queries.GetTransferableTransfersByPkScript(ctx, gen.GetTransferableTransfersByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.EventInscribeTransfer, 0, len(models))
for _, model := range models {
ent, err := mapEventInscribeTransferModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
result = append(result, &ent)
}
return result, nil
}
func (r *Repository) GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error) {
model, err := r.queries.GetDeployEventByTick(ctx, tick)
if err != nil {
return nil, errors.WithStack(err)
}
ent, err := mapEventDeployModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
return &ent, nil
}
func (r *Repository) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (first, last int64, err error) {
model, err := r.queries.GetFirstLastInscriptionNumberByTick(ctx, tick)
if err != nil {
return -1, -1, errors.WithStack(err)
}
return model.FirstInscriptionNumber, model.LastInscriptionNumber, nil
}
func (r *Repository) GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error) {
models, err := r.queries.GetDeployEvents(ctx, gen.GetDeployEventsParams{
FilterPkScript: pkScript != nil,
PkScript: hex.EncodeToString(pkScript),
FilterTicker: tick != "",
Ticker: tick,
BlockHeight: int32(height),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.EventDeploy, 0, len(models))
for _, model := range models {
ent, err := mapEventDeployModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
result = append(result, &ent)
}
return result, nil
}
func (r *Repository) GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error) {
models, err := r.queries.GetMintEvents(ctx, gen.GetMintEventsParams{
FilterPkScript: pkScript != nil,
PkScript: hex.EncodeToString(pkScript),
FilterTicker: tick != "",
Ticker: tick,
BlockHeight: int32(height),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.EventMint, 0, len(models))
for _, model := range models {
ent, err := mapEventMintModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
result = append(result, &ent)
}
return result, nil
}
func (r *Repository) GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error) {
models, err := r.queries.GetInscribeTransferEvents(ctx, gen.GetInscribeTransferEventsParams{
FilterPkScript: pkScript != nil,
PkScript: hex.EncodeToString(pkScript),
FilterTicker: tick != "",
Ticker: tick,
BlockHeight: int32(height),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.EventInscribeTransfer, 0, len(models))
for _, model := range models {
ent, err := mapEventInscribeTransferModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
result = append(result, &ent)
}
return result, nil
}
func (r *Repository) GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error) {
models, err := r.queries.GetTransferTransferEvents(ctx, gen.GetTransferTransferEventsParams{
FilterPkScript: pkScript != nil,
PkScript: hex.EncodeToString(pkScript),
FilterTicker: tick != "",
Ticker: tick,
BlockHeight: int32(height),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make([]*entity.EventTransferTransfer, 0, len(models))
for _, model := range models {
ent, err := mapEventTransferTransferModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event model")
}
result = append(result, &ent)
}
return result, nil
}
func (r *Repository) CreateIndexedBlock(ctx context.Context, block *entity.IndexedBlock) error {
params := mapIndexedBlockTypeToParams(*block)
if err := r.queries.CreateIndexedBlock(ctx, params); err != nil {

View File

@@ -203,6 +203,188 @@ func (q *Queries) GetBalancesBatchAtHeight(ctx context.Context, arg GetBalancesB
return items, nil
}
const getBalancesByPkScript = `-- name: GetBalancesByPkScript :many
WITH balances AS (
SELECT DISTINCT ON (tick) pkscript, block_height, tick, overall_balance, available_balance FROM brc20_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY tick, overall_balance DESC
)
SELECT pkscript, block_height, tick, overall_balance, available_balance FROM balances WHERE overall_balance > 0
`
type GetBalancesByPkScriptParams struct {
Pkscript string
BlockHeight int32
}
type GetBalancesByPkScriptRow struct {
Pkscript string
BlockHeight int32
Tick string
OverallBalance pgtype.Numeric
AvailableBalance pgtype.Numeric
}
func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBalancesByPkScriptRow
for rows.Next() {
var i GetBalancesByPkScriptRow
if err := rows.Scan(
&i.Pkscript,
&i.BlockHeight,
&i.Tick,
&i.OverallBalance,
&i.AvailableBalance,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBalancesByTick = `-- name: GetBalancesByTick :many
WITH balances AS (
SELECT DISTINCT ON (pkscript) pkscript, block_height, tick, overall_balance, available_balance FROM brc20_balances WHERE tick = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
)
SELECT pkscript, block_height, tick, overall_balance, available_balance FROM balances WHERE overall_balance > 0
`
type GetBalancesByTickParams struct {
Tick string
BlockHeight int32
}
type GetBalancesByTickRow struct {
Pkscript string
BlockHeight int32
Tick string
OverallBalance pgtype.Numeric
AvailableBalance pgtype.Numeric
}
func (q *Queries) GetBalancesByTick(ctx context.Context, arg GetBalancesByTickParams) ([]GetBalancesByTickRow, error) {
rows, err := q.db.Query(ctx, getBalancesByTick, arg.Tick, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBalancesByTickRow
for rows.Next() {
var i GetBalancesByTickRow
if err := rows.Scan(
&i.Pkscript,
&i.BlockHeight,
&i.Tick,
&i.OverallBalance,
&i.AvailableBalance,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDeployEventByTick = `-- name: GetDeployEventByTick :one
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, total_supply, decimals, limit_per_mint, is_self_mint FROM brc20_event_deploys WHERE tick = $1
`
func (q *Queries) GetDeployEventByTick(ctx context.Context, tick string) (Brc20EventDeploy, error) {
row := q.db.QueryRow(ctx, getDeployEventByTick, tick)
var i Brc20EventDeploy
err := row.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.TotalSupply,
&i.Decimals,
&i.LimitPerMint,
&i.IsSelfMint,
)
return i, err
}
const getDeployEvents = `-- name: GetDeployEvents :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, total_supply, decimals, limit_per_mint, is_self_mint FROM "brc20_event_deploys"
WHERE (
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR pkscript = $2
) AND (
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
OR tick = $4
) AND (
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
)
`
type GetDeployEventsParams struct {
FilterPkScript bool
PkScript string
FilterTicker bool
Ticker string
BlockHeight int32
}
func (q *Queries) GetDeployEvents(ctx context.Context, arg GetDeployEventsParams) ([]Brc20EventDeploy, error) {
rows, err := q.db.Query(ctx, getDeployEvents,
arg.FilterPkScript,
arg.PkScript,
arg.FilterTicker,
arg.Ticker,
arg.BlockHeight,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventDeploy
for rows.Next() {
var i Brc20EventDeploy
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.TotalSupply,
&i.Decimals,
&i.LimitPerMint,
&i.IsSelfMint,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEventInscribeTransfersByInscriptionIds = `-- name: GetEventInscribeTransfersByInscriptionIds :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, output_index, sats_amount, amount FROM "brc20_event_inscribe_transfers" WHERE "inscription_id" = ANY($1::text[])
`
@@ -242,6 +424,31 @@ func (q *Queries) GetEventInscribeTransfersByInscriptionIds(ctx context.Context,
return items, nil
}
const getFirstLastInscriptionNumberByTick = `-- name: GetFirstLastInscriptionNumberByTick :one
SELECT
COALESCE(MIN("inscription_number"), -1)::BIGINT AS "first_inscription_number",
COALESCE(MAX("inscription_number"), -1)::BIGINT AS "last_inscription_number"
FROM (
SELECT inscription_number FROM "brc20_event_mints" WHERE "brc20_event_mints"."tick" = $1
UNION ALL
SELECT inscription_number FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers"."tick" = $1
UNION ALL
SELECT inscription_number FROM "brc20_event_transfer_transfers" WHERE "brc20_event_transfer_transfers"."tick" = $1
) as events
`
type GetFirstLastInscriptionNumberByTickRow struct {
FirstInscriptionNumber int64
LastInscriptionNumber int64
}
func (q *Queries) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (GetFirstLastInscriptionNumberByTickRow, error) {
row := q.db.QueryRow(ctx, getFirstLastInscriptionNumberByTick, tick)
var i GetFirstLastInscriptionNumberByTickRow
err := row.Scan(&i.FirstInscriptionNumber, &i.LastInscriptionNumber)
return i, err
}
const getIndexedBlockByHeight = `-- name: GetIndexedBlockByHeight :one
SELECT height, hash, event_hash, cumulative_event_hash FROM "brc20_indexed_blocks" WHERE "height" = $1
`
@@ -258,6 +465,68 @@ func (q *Queries) GetIndexedBlockByHeight(ctx context.Context, height int32) (Br
return i, err
}
const getInscribeTransferEvents = `-- name: GetInscribeTransferEvents :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, output_index, sats_amount, amount FROM "brc20_event_inscribe_transfers"
WHERE (
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR pkscript = $2
) AND (
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
OR tick = $4
) AND (
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
)
`
type GetInscribeTransferEventsParams struct {
FilterPkScript bool
PkScript string
FilterTicker bool
Ticker string
BlockHeight int32
}
func (q *Queries) GetInscribeTransferEvents(ctx context.Context, arg GetInscribeTransferEventsParams) ([]Brc20EventInscribeTransfer, error) {
rows, err := q.db.Query(ctx, getInscribeTransferEvents,
arg.FilterPkScript,
arg.PkScript,
arg.FilterTicker,
arg.Ticker,
arg.BlockHeight,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventInscribeTransfer
for rows.Next() {
var i Brc20EventInscribeTransfer
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.OutputIndex,
&i.SatsAmount,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInscriptionEntriesByIds = `-- name: GetInscriptionEntriesByIds :many
WITH "states" AS (
-- select latest state
@@ -528,6 +797,67 @@ func (q *Queries) GetLatestProcessorStats(ctx context.Context) (Brc20ProcessorSt
return i, err
}
const getMintEvents = `-- name: GetMintEvents :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, amount, parent_id FROM "brc20_event_mints"
WHERE (
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR pkscript = $2
) AND (
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
OR tick = $4
) AND (
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
)
`
type GetMintEventsParams struct {
FilterPkScript bool
PkScript string
FilterTicker bool
Ticker string
BlockHeight int32
}
func (q *Queries) GetMintEvents(ctx context.Context, arg GetMintEventsParams) ([]Brc20EventMint, error) {
rows, err := q.db.Query(ctx, getMintEvents,
arg.FilterPkScript,
arg.PkScript,
arg.FilterTicker,
arg.Ticker,
arg.BlockHeight,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventMint
for rows.Next() {
var i Brc20EventMint
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.Amount,
&i.ParentID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTickEntriesByTicks = `-- name: GetTickEntriesByTicks :many
WITH "states" AS (
-- select latest state
@@ -591,3 +921,214 @@ func (q *Queries) GetTickEntriesByTicks(ctx context.Context, ticks []string) ([]
}
return items, nil
}
const getTickEntriesByTicksAndHeight = `-- name: GetTickEntriesByTicksAndHeight :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("tick") tick, block_height, minted_amount, burned_amount, completed_at, completed_at_height FROM "brc20_tick_entry_states" WHERE "tick" = ANY($1::text[]) AND block_height <= $2 ORDER BY "tick", "block_height" DESC
)
SELECT brc20_tick_entries.tick, original_tick, total_supply, decimals, limit_per_mint, is_self_mint, deploy_inscription_id, deployed_at, deployed_at_height, states.tick, block_height, minted_amount, burned_amount, completed_at, completed_at_height FROM "brc20_tick_entries"
LEFT JOIN "states" ON "brc20_tick_entries"."tick" = "states"."tick"
WHERE "brc20_tick_entries"."tick" = ANY($1::text[]) AND deployed_at_height <= $2
`
type GetTickEntriesByTicksAndHeightParams struct {
Ticks []string
Height int32
}
type GetTickEntriesByTicksAndHeightRow struct {
Tick string
OriginalTick string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
DeployInscriptionID string
DeployedAt pgtype.Timestamp
DeployedAtHeight int32
Tick_2 pgtype.Text
BlockHeight pgtype.Int4
MintedAmount pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
// WITH
// "first_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" ASC LIMIT 1),
// "latest_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" DESC LIMIT 1),
// "first_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" ASC LIMIT 1),
// "latest_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" DESC LIMIT 1)
// SELECT
//
// COALESCE(
// LEAST(
// (SELECT "inscription_number" FROM "first_mint"),
// (SELECT "inscription_number" FROM "first_inscribe_transfer")
// ),
// -1
// ) AS "first_inscription_number",
// COALESCE(
// GREATEST(
// (SELECT "inscription_number" FROM "latest_mint"),
// (SELECT "inscription_number" FROM "latest_inscribe_transfer")
// ),
// -1
// ) AS "last_inscription_number";
func (q *Queries) GetTickEntriesByTicksAndHeight(ctx context.Context, arg GetTickEntriesByTicksAndHeightParams) ([]GetTickEntriesByTicksAndHeightRow, error) {
rows, err := q.db.Query(ctx, getTickEntriesByTicksAndHeight, arg.Ticks, arg.Height)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTickEntriesByTicksAndHeightRow
for rows.Next() {
var i GetTickEntriesByTicksAndHeightRow
if err := rows.Scan(
&i.Tick,
&i.OriginalTick,
&i.TotalSupply,
&i.Decimals,
&i.LimitPerMint,
&i.IsSelfMint,
&i.DeployInscriptionID,
&i.DeployedAt,
&i.DeployedAtHeight,
&i.Tick_2,
&i.BlockHeight,
&i.MintedAmount,
&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 getTransferTransferEvents = `-- name: GetTransferTransferEvents :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, from_pkscript, from_satpoint, from_input_index, to_pkscript, to_satpoint, to_output_index, spent_as_fee, amount FROM "brc20_event_transfer_transfers"
WHERE (
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR from_pkscript = $2
OR to_pkscript = $2
) AND (
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
OR tick = $4
) AND (
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
)
`
type GetTransferTransferEventsParams struct {
FilterPkScript bool
PkScript string
FilterTicker bool
Ticker string
BlockHeight int32
}
func (q *Queries) GetTransferTransferEvents(ctx context.Context, arg GetTransferTransferEventsParams) ([]Brc20EventTransferTransfer, error) {
rows, err := q.db.Query(ctx, getTransferTransferEvents,
arg.FilterPkScript,
arg.PkScript,
arg.FilterTicker,
arg.Ticker,
arg.BlockHeight,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventTransferTransfer
for rows.Next() {
var i Brc20EventTransferTransfer
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.FromPkscript,
&i.FromSatpoint,
&i.FromInputIndex,
&i.ToPkscript,
&i.ToSatpoint,
&i.ToOutputIndex,
&i.SpentAsFee,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTransferableTransfersByPkScript = `-- name: GetTransferableTransfersByPkScript :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, output_index, sats_amount, amount
FROM "brc20_event_inscribe_transfers"
WHERE
pkscript = $1
AND "brc20_event_inscribe_transfers"."block_height" <= $2
AND NOT EXISTS (
SELECT NULL
FROM "brc20_event_transfer_transfers"
WHERE "brc20_event_transfer_transfers"."inscription_id" = "brc20_event_inscribe_transfers"."inscription_id"
)
ORDER BY "brc20_event_inscribe_transfers"."block_height" DESC
`
type GetTransferableTransfersByPkScriptParams struct {
Pkscript string
BlockHeight int32
}
func (q *Queries) GetTransferableTransfersByPkScript(ctx context.Context, arg GetTransferableTransfersByPkScriptParams) ([]Brc20EventInscribeTransfer, error) {
rows, err := q.db.Query(ctx, getTransferableTransfersByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventInscribeTransfer
for rows.Next() {
var i Brc20EventInscribeTransfer
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.OutputIndex,
&i.SatsAmount,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -0,0 +1,24 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
)
func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error) {
balances, err := u.dg.GetBalancesByPkScript(ctx, pkScript, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
return balances, nil
}
func (u *Usecase) GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error) {
balances, err := u.dg.GetBalancesByTick(ctx, tick, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "failed to get balance by tick")
}
return balances, nil
}

View File

@@ -0,0 +1,33 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
)
func (u *Usecase) GetTickEntryByTickBatch(ctx context.Context, ticks []string) (map[string]*entity.TickEntry, error) {
entries, err := u.dg.GetTickEntriesByTicks(ctx, ticks)
if err != nil {
return nil, errors.Wrap(err, "error during GetTickEntriesByTicks")
}
return entries, nil
}
func (u *Usecase) GetTickEntryByTickAndHeight(ctx context.Context, tick string, blockHeight uint64) (*entity.TickEntry, error) {
entries, err := u.GetTickEntryByTickAndHeightBatch(ctx, []string{tick}, blockHeight)
if err != nil {
return nil, errors.WithStack(err)
}
entry, ok := entries[tick]
if !ok {
return nil, errors.Wrap(errs.NotFound, "entry not found")
}
return entry, nil
}
func (u *Usecase) GetTickEntryByTickAndHeightBatch(ctx context.Context, ticks []string, blockHeight uint64) (map[string]*entity.TickEntry, error) {
return nil, nil
}

View File

@@ -0,0 +1,15 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
)
func (u *Usecase) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (int64, int64, error) {
first, last, err := u.dg.GetFirstLastInscriptionNumberByTick(ctx, tick)
if err != nil {
return -1, -1, errors.Wrap(err, "error during GetFirstLastInscriptionNumberByTick")
}
return first, last, nil
}

View File

@@ -0,0 +1,16 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
)
func (u *Usecase) GetLatestBlock(ctx context.Context) (types.BlockHeader, error) {
blockHeader, err := u.dg.GetLatestBlock(ctx)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "failed to get latest block")
}
return blockHeader, nil
}

View File

@@ -0,0 +1,16 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
)
func (u *Usecase) GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error) {
result, err := u.dg.GetDeployEventByTick(ctx, tick)
if err != nil {
return nil, errors.Wrap(err, "error during GetDeployEventByTick")
}
return result, nil
}

View File

@@ -0,0 +1,40 @@
package usecase
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
)
func (u *Usecase) GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error) {
result, err := u.dg.GetDeployEvents(ctx, pkScript, tick, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetDeployEvents")
}
return result, nil
}
func (u *Usecase) GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error) {
result, err := u.dg.GetMintEvents(ctx, pkScript, tick, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetMintEvents")
}
return result, nil
}
func (u *Usecase) GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error) {
result, err := u.dg.GetInscribeTransferEvents(ctx, pkScript, tick, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetInscribeTransferEvents")
}
return result, nil
}
func (u *Usecase) GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error) {
result, err := u.dg.GetTransferTransferEvents(ctx, pkScript, tick, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetTransferTransfersEvents")
}
return result, nil
}

View File

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

View File

@@ -0,0 +1,18 @@
package usecase
import (
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/pkg/btcclient"
)
type Usecase struct {
dg datagateway.BRC20DataGateway
bitcoinClient btcclient.Contract
}
func New(dg datagateway.BRC20DataGateway, bitcoinClient btcclient.Contract) *Usecase {
return &Usecase{
dg: dg,
bitcoinClient: bitcoinClient,
}
}

View File

@@ -1,6 +1,7 @@
package decimals
import (
"math"
"math/big"
"reflect"
@@ -10,6 +11,7 @@ import (
"github.com/gaze-network/uint128"
"github.com/holiman/uint256"
"github.com/shopspring/decimal"
"golang.org/x/exp/constraints"
)
const (
@@ -27,7 +29,7 @@ func MustFromString(s string) decimal.Decimal {
}
// ToDecimal convert any type to decimal.Decimal (safety floating point)
func ToDecimal(ivalue any, decimals uint16) decimal.Decimal {
func ToDecimal[T constraints.Integer](ivalue any, decimals T) decimal.Decimal {
value := new(big.Int)
switch v := ivalue.(type) {
case string:
@@ -53,6 +55,14 @@ func ToDecimal(ivalue any, decimals uint16) decimal.Decimal {
case *uint256.Int:
value = v.ToBig()
}
switch {
case int64(decimals) > math.MaxInt32:
logger.Panic("ToDecimal: decimals is too big, should be equal less than 2^31-1", slogx.Any("decimals", decimals))
case int64(decimals) < math.MinInt32+1:
logger.Panic("ToDecimal: decimals is too small, should be greater than -2^31", slogx.Any("decimals", decimals))
}
return decimal.NewFromBigInt(value, -int32(decimals))
}
@@ -87,7 +97,7 @@ func ToBigInt(iamount any, decimals uint16) *big.Int {
func ToUint256(iamount any, decimals uint16) *uint256.Int {
result := new(uint256.Int)
if overflow := result.SetFromBig(ToBigInt(iamount, decimals)); overflow {
logger.Panic("ToUint256 overflow", slogx.Any("amount", iamount), slogx.Uint16("decimals", decimals))
logger.Panic("ToUint256: overflow", slogx.Any("amount", iamount), slogx.Uint16("decimals", decimals))
}
return result
}

View File

@@ -12,6 +12,12 @@ import (
)
func TestToDecimal(t *testing.T) {
t.Run("overflow_decimals", func(t *testing.T) {
assert.NotPanics(t, func() { ToDecimal(1, math.MaxInt32-1) }, "in-range decimals shouldn't panic")
assert.NotPanics(t, func() { ToDecimal(1, math.MinInt32+1) }, "in-range decimals shouldn't panic")
assert.Panics(t, func() { ToDecimal(1, math.MaxInt32+1) }, "out of range decimals should panic")
assert.Panics(t, func() { ToDecimal(1, math.MinInt32) }, "out of range decimals should panic")
})
t.Run("check_supported_types", func(t *testing.T) {
testcases := []struct {
decimals uint16