mirror of
https://github.com/alexgo-io/gaze-brc20-indexer.git
synced 2026-01-12 22:22:19 +08:00
feat(brc20): implement brc-20 indexer api
This commit is contained in:
11
modules/brc20/api/api.go
Normal file
11
modules/brc20/api/api.go
Normal 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)
|
||||
}
|
||||
115
modules/brc20/api/httphandler/get_balances_by_address.go
Normal file
115
modules/brc20/api/httphandler/get_balances_by_address.go
Normal 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))
|
||||
}
|
||||
125
modules/brc20/api/httphandler/get_balances_by_address_batch.go
Normal file
125
modules/brc20/api/httphandler/get_balances_by_address_batch.go
Normal 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))
|
||||
}
|
||||
49
modules/brc20/api/httphandler/get_current_block.go
Normal file
49
modules/brc20/api/httphandler/get_current_block.go
Normal 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))
|
||||
}
|
||||
107
modules/brc20/api/httphandler/get_holders.go
Normal file
107
modules/brc20/api/httphandler/get_holders.go
Normal 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))
|
||||
}
|
||||
149
modules/brc20/api/httphandler/get_token_info.go
Normal file
149
modules/brc20/api/httphandler/get_token_info.go
Normal 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))
|
||||
}
|
||||
465
modules/brc20/api/httphandler/get_transactions.go
Normal file
465
modules/brc20/api/httphandler/get_transactions.go
Normal 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))
|
||||
}
|
||||
135
modules/brc20/api/httphandler/get_utxos_by_address.go
Normal file
135
modules/brc20/api/httphandler/get_utxos_by_address.go
Normal 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))
|
||||
}
|
||||
18
modules/brc20/api/httphandler/httphandler.go
Normal file
18
modules/brc20/api/httphandler/httphandler.go
Normal 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,
|
||||
}
|
||||
}
|
||||
19
modules/brc20/api/httphandler/routes.go
Normal file
19
modules/brc20/api/httphandler/routes.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
24
modules/brc20/internal/usecase/get_balances.go
Normal file
24
modules/brc20/internal/usecase/get_balances.go
Normal 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
|
||||
}
|
||||
33
modules/brc20/internal/usecase/get_entry.go
Normal file
33
modules/brc20/internal/usecase/get_entry.go
Normal 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
|
||||
}
|
||||
15
modules/brc20/internal/usecase/get_inscription_number.go
Normal file
15
modules/brc20/internal/usecase/get_inscription_number.go
Normal 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
|
||||
}
|
||||
16
modules/brc20/internal/usecase/get_latest_block.go
Normal file
16
modules/brc20/internal/usecase/get_latest_block.go
Normal 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
|
||||
}
|
||||
16
modules/brc20/internal/usecase/get_tick_events.go
Normal file
16
modules/brc20/internal/usecase/get_tick_events.go
Normal 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
|
||||
}
|
||||
40
modules/brc20/internal/usecase/get_transactions.go
Normal file
40
modules/brc20/internal/usecase/get_transactions.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
18
modules/brc20/internal/usecase/usecase.go
Normal file
18
modules/brc20/internal/usecase/usecase.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user