mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-03-26 22:37:40 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a0fe84e40 | ||
|
|
f1d4651042 | ||
|
|
5f4f50a9e5 | ||
|
|
32c3c5c1d4 | ||
|
|
2a572e6d1e | ||
|
|
aa25a6882b | ||
|
|
6182c63150 | ||
|
|
e1f8eaa3e1 | ||
|
|
107836ae39 | ||
|
|
1bd84b0154 | ||
|
|
de26a4c21d | ||
|
|
1dc57d74e0 | ||
|
|
7c0e28d8ea | ||
|
|
754fd1e997 | ||
|
|
66f03f7107 | ||
|
|
7a863987ec | ||
|
|
f9c6ef8dfd |
2
.github/workflows/sqlc-verify.yml
vendored
2
.github/workflows/sqlc-verify.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup Sqlc
|
||||
uses: sqlc-dev/setup-sqlc@v4
|
||||
with:
|
||||
sqlc-version: "1.26.0"
|
||||
sqlc-version: "1.27.0"
|
||||
|
||||
- name: Check Diff
|
||||
run: sqlc diff
|
||||
|
||||
@@ -101,3 +101,6 @@ linters-settings:
|
||||
attr-only: true
|
||||
key-naming-case: snake
|
||||
args-on-sep-lines: true
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
@@ -24,6 +24,9 @@ var (
|
||||
// Skippable is returned when got an error but it can be skipped or ignored and continue
|
||||
Skippable = errors.NewWithDepth(depth, "skippable")
|
||||
|
||||
// Retryable is returned when got an error but it can be retried
|
||||
Retryable = errors.NewWithDepth(depth, "retryable")
|
||||
|
||||
// Unsupported is returned when a feature or result is not supported
|
||||
Unsupported = errors.NewWithDepth(depth, "unsupported")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/datasources"
|
||||
@@ -142,7 +143,7 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
|
||||
// validate reorg from first input
|
||||
{
|
||||
remoteBlockHeader := firstInputHeader
|
||||
if !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
|
||||
if i.currentBlock.Hash != (chainhash.Hash{}) && !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
|
||||
logger.WarnContext(ctx, "Detected chain reorganization. Searching for fork point...",
|
||||
slogx.String("event", "reorg_detected"),
|
||||
slogx.Stringer("current_hash", i.currentBlock.Hash),
|
||||
@@ -215,7 +216,7 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
|
||||
return errors.Wrapf(errs.InternalError, "input is not continuous, input[%d] height: %d, input[%d] height: %d", i-1, prevHeader.Height, i, header.Height)
|
||||
}
|
||||
|
||||
if !header.PrevBlock.IsEqual(&prevHeader.Hash) {
|
||||
if prevHeader.Hash != (chainhash.Hash{}) && !header.PrevBlock.IsEqual(&prevHeader.Hash) {
|
||||
logger.WarnContext(ctx, "Chain Reorganization occurred in the middle of batch fetching inputs, need to try to fetch again")
|
||||
|
||||
// end current round
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: blocks.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: events.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: nodes.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: nodesales.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: test.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
@@ -11,11 +13,10 @@ import (
|
||||
)
|
||||
|
||||
type getBalancesRequest struct {
|
||||
paginationRequest
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -23,13 +24,20 @@ const (
|
||||
getBalancesDefaultLimit = 100
|
||||
)
|
||||
|
||||
func (r getBalancesRequest) Validate() error {
|
||||
func (r *getBalancesRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
if r.Id != "" {
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
}
|
||||
if r.Limit < 0 {
|
||||
errList = append(errList, errors.New("'limit' must be non-negative"))
|
||||
@@ -66,8 +74,8 @@ func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getBalancesDefaultLimit
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pkScript, ok := resolvePkScript(h.network, req.Wallet)
|
||||
|
||||
@@ -40,7 +40,7 @@ func (r getBalancesBatchRequest) Validate() error {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required", i))
|
||||
}
|
||||
if query.Id != "" && !isRuneIdOrRuneName(query.Id) {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'id' is not valid rune id or rune name", i))
|
||||
errList = append(errList, errors.Errorf("queries[%d]: id '%s' is not valid rune id or rune name", i, query.Id))
|
||||
}
|
||||
if query.Limit < 0 {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'limit' must be non-negative", i))
|
||||
@@ -89,7 +89,7 @@ func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
|
||||
}
|
||||
|
||||
if query.Limit == 0 {
|
||||
query.Limit = getBalancesMaxLimit
|
||||
query.Limit = getBalancesDefaultLimit
|
||||
}
|
||||
|
||||
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight, query.Limit, query.Offset)
|
||||
|
||||
@@ -3,6 +3,7 @@ package httphandler
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
@@ -15,21 +16,24 @@ import (
|
||||
)
|
||||
|
||||
type getHoldersRequest struct {
|
||||
paginationRequest
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
getHoldersMaxLimit = 1000
|
||||
getHoldersDefaultLimit = 100
|
||||
getHoldersMaxLimit = 1000
|
||||
)
|
||||
|
||||
func (r getHoldersRequest) Validate() error {
|
||||
func (r *getHoldersRequest) Validate() error {
|
||||
var errList []error
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
if r.Limit < 0 {
|
||||
errList = append(errList, errors.New("'limit' must be non-negative"))
|
||||
@@ -68,6 +72,9 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
@@ -78,10 +85,6 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getHoldersDefaultLimit
|
||||
}
|
||||
|
||||
var runeId runes.RuneId
|
||||
if req.Id != "" {
|
||||
var ok bool
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
@@ -17,10 +18,15 @@ type getTokenInfoRequest struct {
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getTokenInfoRequest) Validate() error {
|
||||
func (r *getTokenInfoRequest) Validate() error {
|
||||
var errList []error
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
@@ -57,9 +63,9 @@ type getTokenInfoResult struct {
|
||||
MintedAmount uint128.Uint128 `json:"mintedAmount"`
|
||||
BurnedAmount uint128.Uint128 `json:"burnedAmount"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
DeployedAt uint64 `json:"deployedAt"` // unix timestamp
|
||||
DeployedAt int64 `json:"deployedAt"` // unix timestamp
|
||||
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
||||
CompletedAt *uint64 `json:"completedAt"` // unix timestamp
|
||||
CompletedAt *int64 `json:"completedAt"` // unix timestamp
|
||||
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
||||
HoldersCount int `json:"holdersCount"`
|
||||
Extend tokenInfoExtend `json:"extend"`
|
||||
@@ -144,9 +150,9 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: runeEntry.BurnedAmount,
|
||||
Decimals: runeEntry.Divisibility,
|
||||
DeployedAt: uint64(runeEntry.EtchedAt.Unix()),
|
||||
DeployedAt: runeEntry.EtchedAt.Unix(),
|
||||
DeployedAtHeight: runeEntry.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(uint64(runeEntry.CompletedAt.Unix()))),
|
||||
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
|
||||
CompletedAtHeight: runeEntry.CompletedAtHeight,
|
||||
HoldersCount: len(holdingBalances),
|
||||
Extend: tokenInfoExtend{
|
||||
|
||||
172
modules/runes/api/httphandler/get_tokens.go
Normal file
172
modules/runes/api/httphandler/get_tokens.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
getTokensMaxLimit = 1000
|
||||
)
|
||||
|
||||
type GetTokensScope string
|
||||
|
||||
const (
|
||||
GetTokensScopeAll GetTokensScope = "all"
|
||||
GetTokensScopeOngoing GetTokensScope = "ongoing"
|
||||
)
|
||||
|
||||
func (s GetTokensScope) IsValid() bool {
|
||||
switch s {
|
||||
case GetTokensScopeAll, GetTokensScopeOngoing:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type getTokensRequest struct {
|
||||
paginationRequest
|
||||
Search string `query:"search"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Scope GetTokensScope `query:"scope"`
|
||||
}
|
||||
|
||||
func (req getTokensRequest) Validate() error {
|
||||
var errList []error
|
||||
if err := req.paginationRequest.Validate(); err != nil {
|
||||
errList = append(errList, err)
|
||||
}
|
||||
if req.Limit > getTokensMaxLimit {
|
||||
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
|
||||
}
|
||||
if req.Scope != "" && !req.Scope.IsValid() {
|
||||
errList = append(errList, errors.Errorf("invalid scope: %s", req.Scope))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
func (req *getTokensRequest) ParseDefault() error {
|
||||
if err := req.paginationRequest.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if req.Scope == "" {
|
||||
req.Scope = GetTokensScopeAll
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type getTokensResult struct {
|
||||
List []getTokenInfoResult `json:"list"`
|
||||
}
|
||||
|
||||
type getTokensResponse = HttpResponse[getTokensResult]
|
||||
|
||||
func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokensRequest
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
// remove spacers
|
||||
search := strings.Replace(strings.Replace(req.Search, "•", "", -1), ".", "", -1)
|
||||
|
||||
var entries []*runes.RuneEntry
|
||||
switch req.Scope {
|
||||
case GetTokensScopeAll:
|
||||
entries, err = h.usecase.GetRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryList")
|
||||
}
|
||||
case GetTokensScopeOngoing:
|
||||
entries, err = h.usecase.GetOngoingRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryList")
|
||||
}
|
||||
default:
|
||||
return errs.NewPublicError(fmt.Sprintf("invalid scope: %s", req.Scope))
|
||||
}
|
||||
|
||||
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
|
||||
totalHolders, err := h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
||||
}
|
||||
|
||||
result := make([]getTokenInfoResult, 0, len(entries))
|
||||
for _, ent := range entries {
|
||||
totalSupply, err := ent.Supply()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get total supply of rune")
|
||||
}
|
||||
mintedAmount, err := ent.MintedAmount()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
||||
}
|
||||
circulatingSupply := mintedAmount.Sub(ent.BurnedAmount)
|
||||
|
||||
terms := lo.FromPtr(ent.Terms)
|
||||
result = append(result, getTokenInfoResult{
|
||||
Id: ent.RuneId,
|
||||
Name: ent.SpacedRune,
|
||||
Symbol: string(ent.Symbol),
|
||||
TotalSupply: totalSupply,
|
||||
CirculatingSupply: circulatingSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: ent.BurnedAmount,
|
||||
Decimals: ent.Divisibility,
|
||||
DeployedAt: ent.EtchedAt.Unix(),
|
||||
DeployedAtHeight: ent.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(ent.CompletedAt.IsZero(), nil, lo.ToPtr(ent.CompletedAt.Unix())),
|
||||
CompletedAtHeight: ent.CompletedAtHeight,
|
||||
HoldersCount: int(totalHolders[ent.RuneId]),
|
||||
Extend: tokenInfoExtend{
|
||||
Entry: entry{
|
||||
Divisibility: ent.Divisibility,
|
||||
Premine: ent.Premine,
|
||||
Rune: ent.SpacedRune.Rune,
|
||||
Spacers: ent.SpacedRune.Spacers,
|
||||
Symbol: string(ent.Symbol),
|
||||
Terms: entryTerms{
|
||||
Amount: lo.FromPtr(terms.Amount),
|
||||
Cap: lo.FromPtr(terms.Cap),
|
||||
HeightStart: terms.HeightStart,
|
||||
HeightEnd: terms.HeightEnd,
|
||||
OffsetStart: terms.OffsetStart,
|
||||
OffsetEnd: terms.OffsetEnd,
|
||||
},
|
||||
Turbo: ent.Turbo,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(getTokensResponse{
|
||||
Result: &getTokensResult{
|
||||
List: result,
|
||||
},
|
||||
}))
|
||||
}
|
||||
171
modules/runes/api/httphandler/get_transaction_by_hash.go
Normal file
171
modules/runes/api/httphandler/get_transaction_by_hash.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getTransactionByHashRequest struct {
|
||||
Hash string `params:"hash"`
|
||||
}
|
||||
|
||||
func (r getTransactionByHashRequest) Validate() error {
|
||||
var errList []error
|
||||
if len(r.Hash) == 0 {
|
||||
errList = append(errList, errs.NewPublicError("hash is required"))
|
||||
}
|
||||
if len(r.Hash) > chainhash.MaxHashStringSize {
|
||||
errList = append(errList, errs.NewPublicError(fmt.Sprintf("hash length must be less than or equal to %d bytes", chainhash.MaxHashStringSize)))
|
||||
}
|
||||
if len(errList) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getTransactionByHashResponse = HttpResponse[transaction]
|
||||
|
||||
func (h *HttpHandler) GetTransactionByHash(ctx *fiber.Ctx) (err error) {
|
||||
var req getTransactionByHashRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
hash, err := chainhash.NewHashFromStr(req.Hash)
|
||||
if err != nil {
|
||||
return errs.NewPublicError("invalid transaction hash")
|
||||
}
|
||||
|
||||
tx, err := h.usecase.GetRuneTransaction(ctx.UserContext(), *hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "transaction not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetRuneTransaction")
|
||||
}
|
||||
|
||||
allRuneIds := make(map[runes.RuneId]struct{})
|
||||
for id := range tx.Mints {
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for id := range tx.Burns {
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
allRuneIds[input.RuneId] = struct{}{}
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
allRuneIds[output.RuneId] = struct{}{}
|
||||
}
|
||||
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
|
||||
}
|
||||
|
||||
respTx := &transaction{
|
||||
TxHash: tx.Hash,
|
||||
BlockHeight: tx.BlockHeight,
|
||||
Index: tx.Index,
|
||||
Timestamp: tx.Timestamp.Unix(),
|
||||
Inputs: make([]txInputOutput, 0, len(tx.Inputs)),
|
||||
Outputs: make([]txInputOutput, 0, len(tx.Outputs)),
|
||||
Mints: make(map[string]amountWithDecimal, len(tx.Mints)),
|
||||
Burns: make(map[string]amountWithDecimal, len(tx.Burns)),
|
||||
Extend: runeTransactionExtend{
|
||||
RuneEtched: tx.RuneEtched,
|
||||
Runestone: nil,
|
||||
},
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
address := addressFromPkScript(input.PkScript, h.network)
|
||||
respTx.Inputs = append(respTx.Inputs, txInputOutput{
|
||||
PkScript: hex.EncodeToString(input.PkScript),
|
||||
Address: address,
|
||||
Id: input.RuneId,
|
||||
Amount: input.Amount,
|
||||
Decimals: runeEntries[input.RuneId].Divisibility,
|
||||
Index: input.Index,
|
||||
})
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
address := addressFromPkScript(output.PkScript, h.network)
|
||||
respTx.Outputs = append(respTx.Outputs, txInputOutput{
|
||||
PkScript: hex.EncodeToString(output.PkScript),
|
||||
Address: address,
|
||||
Id: output.RuneId,
|
||||
Amount: output.Amount,
|
||||
Decimals: runeEntries[output.RuneId].Divisibility,
|
||||
Index: output.Index,
|
||||
})
|
||||
}
|
||||
for id, amount := range tx.Mints {
|
||||
respTx.Mints[id.String()] = amountWithDecimal{
|
||||
Amount: amount,
|
||||
Decimals: runeEntries[id].Divisibility,
|
||||
}
|
||||
}
|
||||
for id, amount := range tx.Burns {
|
||||
respTx.Burns[id.String()] = amountWithDecimal{
|
||||
Amount: amount,
|
||||
Decimals: runeEntries[id].Divisibility,
|
||||
}
|
||||
}
|
||||
if tx.Runestone != nil {
|
||||
var e *etching
|
||||
if tx.Runestone.Etching != nil {
|
||||
var symbol *string
|
||||
if tx.Runestone.Etching.Symbol != nil {
|
||||
symbol = lo.ToPtr(string(*tx.Runestone.Etching.Symbol))
|
||||
}
|
||||
var t *terms
|
||||
if tx.Runestone.Etching.Terms != nil {
|
||||
t = &terms{
|
||||
Amount: tx.Runestone.Etching.Terms.Amount,
|
||||
Cap: tx.Runestone.Etching.Terms.Cap,
|
||||
HeightStart: tx.Runestone.Etching.Terms.HeightStart,
|
||||
HeightEnd: tx.Runestone.Etching.Terms.HeightEnd,
|
||||
OffsetStart: tx.Runestone.Etching.Terms.OffsetStart,
|
||||
OffsetEnd: tx.Runestone.Etching.Terms.OffsetEnd,
|
||||
}
|
||||
}
|
||||
e = &etching{
|
||||
Divisibility: tx.Runestone.Etching.Divisibility,
|
||||
Premine: tx.Runestone.Etching.Premine,
|
||||
Rune: tx.Runestone.Etching.Rune,
|
||||
Spacers: tx.Runestone.Etching.Spacers,
|
||||
Symbol: symbol,
|
||||
Terms: t,
|
||||
Turbo: tx.Runestone.Etching.Turbo,
|
||||
}
|
||||
}
|
||||
respTx.Extend.Runestone = &runestone{
|
||||
Cenotaph: tx.Runestone.Cenotaph,
|
||||
Flaws: lo.Ternary(tx.Runestone.Cenotaph, tx.Runestone.Flaws.CollectAsString(), nil),
|
||||
Etching: e,
|
||||
Edicts: lo.Map(tx.Runestone.Edicts, func(ed runes.Edict, _ int) edict {
|
||||
return edict{
|
||||
Id: ed.Id,
|
||||
Amount: ed.Amount,
|
||||
Output: ed.Output,
|
||||
}
|
||||
}),
|
||||
Mint: tx.Runestone.Mint,
|
||||
Pointer: tx.Runestone.Pointer,
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(getTransactionByHashResponse{
|
||||
Result: respTx,
|
||||
}))
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"cmp"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
@@ -16,23 +17,28 @@ import (
|
||||
)
|
||||
|
||||
type getTransactionsRequest struct {
|
||||
paginationRequest
|
||||
Wallet string `query:"wallet"`
|
||||
Id string `query:"id"`
|
||||
FromBlock int64 `query:"fromBlock"`
|
||||
ToBlock int64 `query:"toBlock"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
getTransactionsMaxLimit = 3000
|
||||
getTransactionsDefaultLimit = 100
|
||||
getTransactionsMaxLimit = 3000
|
||||
)
|
||||
|
||||
func (r getTransactionsRequest) Validate() error {
|
||||
func (r *getTransactionsRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
if r.Id != "" {
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
}
|
||||
if r.FromBlock < -1 {
|
||||
errList = append(errList, errors.Errorf("invalid fromBlock range"))
|
||||
@@ -128,6 +134,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var pkScript []byte
|
||||
if req.Wallet != "" {
|
||||
@@ -146,9 +155,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
}
|
||||
}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getTransactionsDefaultLimit
|
||||
}
|
||||
|
||||
// default to latest block
|
||||
if req.ToBlock == 0 {
|
||||
@@ -185,23 +191,22 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
return errors.Wrap(err, "error during GetRuneTransactions")
|
||||
}
|
||||
|
||||
var allRuneIds []runes.RuneId
|
||||
allRuneIds := make(map[runes.RuneId]struct{})
|
||||
for _, tx := range txs {
|
||||
for id := range tx.Mints {
|
||||
allRuneIds = append(allRuneIds, id)
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for id := range tx.Burns {
|
||||
allRuneIds = append(allRuneIds, id)
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
allRuneIds = append(allRuneIds, input.RuneId)
|
||||
allRuneIds[input.RuneId] = struct{}{}
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
allRuneIds = append(allRuneIds, output.RuneId)
|
||||
allRuneIds[output.RuneId] = struct{}{}
|
||||
}
|
||||
}
|
||||
allRuneIds = lo.Uniq(allRuneIds)
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds)
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune entries not found")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
@@ -12,25 +14,30 @@ import (
|
||||
)
|
||||
|
||||
type getUTXOsRequest struct {
|
||||
paginationRequest
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
getUTXOsMaxLimit = 3000
|
||||
getUTXOsDefaultLimit = 100
|
||||
getUTXOsMaxLimit = 3000
|
||||
)
|
||||
|
||||
func (r getUTXOsRequest) Validate() error {
|
||||
func (r *getUTXOsRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
if r.Id != "" {
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
}
|
||||
if r.Limit < 0 {
|
||||
errList = append(errList, errors.New("'limit' must be non-negative"))
|
||||
@@ -78,16 +85,15 @@ func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pkScript, ok := resolvePkScript(h.network, req.Wallet)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getUTXOsDefaultLimit
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/usecase"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
@@ -31,6 +33,53 @@ type HttpResponse[T any] struct {
|
||||
Result *T `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type paginationRequest struct {
|
||||
Offset int32 `query:"offset"`
|
||||
Limit int32 `query:"limit"`
|
||||
|
||||
// OrderBy string `query:"orderBy"` // ASC or DESC
|
||||
// SortBy string `query:"sortBy"` // column name
|
||||
}
|
||||
|
||||
func (req paginationRequest) Validate() error {
|
||||
var errList []error
|
||||
|
||||
// this just safeguard for limit,
|
||||
// each path should have own validation.
|
||||
if req.Limit > 10000 {
|
||||
errList = append(errList, errors.Errorf("too large limit"))
|
||||
}
|
||||
if req.Limit < 0 {
|
||||
errList = append(errList, errors.Errorf("limit must be greater than or equal to 0"))
|
||||
}
|
||||
if req.Offset < 0 {
|
||||
errList = append(errList, errors.Errorf("offset must be greater than or equal to 0"))
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// if req.OrderBy != "" && req.OrderBy != "ASC" && req.OrderBy != "DESC" {
|
||||
// errList = append(errList, errors.Errorf("invalid orderBy value, must be `ASC` or `DESC`"))
|
||||
// }
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "pagination validation error")
|
||||
}
|
||||
|
||||
func (req *paginationRequest) ParseDefault() error {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// if req.OrderBy == "" {
|
||||
// req.OrderBy = "ASC"
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolvePkScript(network common.Network, wallet string) ([]byte, bool) {
|
||||
if wallet == "" {
|
||||
return nil, false
|
||||
|
||||
@@ -10,11 +10,13 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
|
||||
r.Post("/balances/wallet/batch", h.GetBalancesBatch)
|
||||
r.Get("/balances/wallet/:wallet", h.GetBalances)
|
||||
r.Get("/transactions", h.GetTransactions)
|
||||
r.Get("/transactions/hash/:hash", h.GetTransactionByHash)
|
||||
r.Get("/holders/:id", h.GetHolders)
|
||||
r.Get("/info/:id", h.GetTokenInfo)
|
||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
|
||||
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
|
||||
r.Get("/utxos/output/:txHash", h.GetUTXOsOutputByLocation)
|
||||
r.Get("/block", h.GetCurrentBlock)
|
||||
r.Get("/tokens", h.GetTokens)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package constants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,22 +19,93 @@ const (
|
||||
EventHashVersion = 1
|
||||
)
|
||||
|
||||
// starting block heights and hashes should be 1 block before activation block, as indexer will start from the block after this value
|
||||
var StartingBlockHeader = map[common.Network]types.BlockHeader{
|
||||
common.NetworkMainnet: {
|
||||
Height: 839999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("0000000000000000000172014ba58d66455762add0512355ad651207918494ab")),
|
||||
},
|
||||
common.NetworkTestnet: {
|
||||
Height: 2519999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("000000000006f45c16402f05d9075db49d3571cf5273cf4cbeaa2aa295f7c833")),
|
||||
},
|
||||
common.NetworkFractalMainnet: {
|
||||
Height: 83999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")), // TODO: Update this to match real hash
|
||||
},
|
||||
common.NetworkFractalTestnet: {
|
||||
Height: 83999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000000613ddfbdd1778b17cea3818febcbbf82762eafaa9461038343")),
|
||||
},
|
||||
}
|
||||
|
||||
type GenesisRuneConfig struct {
|
||||
RuneId runes.RuneId
|
||||
Name string
|
||||
Number uint64
|
||||
Divisibility uint8
|
||||
Premine uint128.Uint128
|
||||
SpacedRune runes.SpacedRune
|
||||
Symbol rune
|
||||
Terms *runes.Terms
|
||||
Turbo bool
|
||||
EtchingTxHash chainhash.Hash
|
||||
EtchedAt time.Time
|
||||
}
|
||||
|
||||
var GenesisRuneConfigMap = map[common.Network]GenesisRuneConfig{
|
||||
common.NetworkMainnet: {
|
||||
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
|
||||
Number: 0,
|
||||
Divisibility: 0,
|
||||
Premine: uint128.Zero,
|
||||
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
|
||||
Symbol: '\u29c9',
|
||||
Terms: &runes.Terms{
|
||||
Amount: lo.ToPtr(uint128.From64(1)),
|
||||
Cap: &uint128.Max,
|
||||
HeightStart: lo.ToPtr(uint64(840000)),
|
||||
HeightEnd: lo.ToPtr(uint64(1050000)),
|
||||
OffsetStart: nil,
|
||||
OffsetEnd: nil,
|
||||
},
|
||||
Turbo: true,
|
||||
EtchingTxHash: chainhash.Hash{},
|
||||
EtchedAt: time.Unix(0, 0),
|
||||
},
|
||||
common.NetworkFractalMainnet: {
|
||||
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
|
||||
Number: 0,
|
||||
Divisibility: 0,
|
||||
Premine: uint128.Zero,
|
||||
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
|
||||
Symbol: '\u29c9',
|
||||
Terms: &runes.Terms{
|
||||
Amount: lo.ToPtr(uint128.From64(1)),
|
||||
Cap: &uint128.Max,
|
||||
HeightStart: lo.ToPtr(uint64(84000)),
|
||||
HeightEnd: lo.ToPtr(uint64(2184000)),
|
||||
OffsetStart: nil,
|
||||
OffsetEnd: nil,
|
||||
},
|
||||
Turbo: true,
|
||||
EtchingTxHash: chainhash.Hash{},
|
||||
EtchedAt: time.Unix(0, 0),
|
||||
},
|
||||
common.NetworkFractalTestnet: {
|
||||
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
|
||||
Number: 0,
|
||||
Divisibility: 0,
|
||||
Premine: uint128.Zero,
|
||||
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
|
||||
Symbol: '\u29c9',
|
||||
Terms: &runes.Terms{
|
||||
Amount: lo.ToPtr(uint128.From64(1)),
|
||||
Cap: &uint128.Max,
|
||||
HeightStart: lo.ToPtr(uint64(84000)),
|
||||
HeightEnd: lo.ToPtr(uint64(2184000)),
|
||||
OffsetStart: nil,
|
||||
OffsetEnd: nil,
|
||||
},
|
||||
Turbo: true,
|
||||
EtchingTxHash: chainhash.Hash{},
|
||||
EtchedAt: time.Unix(0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE EXTENSION pg_trgm;
|
||||
-- Indexer Client Information
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "runes_indexer_stats" (
|
||||
@@ -48,6 +49,7 @@ CREATE TABLE IF NOT EXISTS "runes_entries" (
|
||||
"etched_at" TIMESTAMP NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_rune_idx ON "runes_entries" USING BTREE ("rune");
|
||||
CREATE INDEX IF NOT EXISTS runes_entries_rune_gin_idx ON "runes_entries" USING GIN ("rune" gin_trgm_ops); -- to speed up queries with LIKE operator
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_number_idx ON "runes_entries" USING BTREE ("number");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "runes_entry_states" (
|
||||
|
||||
@@ -13,6 +13,12 @@ SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3
|
||||
-- name: GetBalanceByPkScriptAndRuneId :one
|
||||
SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_height <= $3 ORDER BY block_height DESC LIMIT 1;
|
||||
|
||||
-- name: GetTotalHoldersByRuneIds :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (rune_id, pkscript) * FROM runes_balances WHERE rune_id = ANY(@rune_ids::TEXT[]) AND block_height <= @block_height ORDER BY rune_id, pkscript, block_height DESC
|
||||
)
|
||||
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id;
|
||||
|
||||
-- name: GetOutPointBalancesAtOutPoint :many
|
||||
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
|
||||
|
||||
@@ -57,6 +63,47 @@ SELECT * FROM runes_entries
|
||||
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
|
||||
WHERE runes_entries.rune_id = ANY(@rune_ids::text[]) AND etching_block <= @height;
|
||||
|
||||
-- name: GetRuneEntries :many
|
||||
WITH states AS (
|
||||
-- select latest state
|
||||
SELECT DISTINCT ON (rune_id) * FROM runes_entry_states WHERE block_height <= @height ORDER BY rune_id, block_height DESC
|
||||
)
|
||||
SELECT * FROM runes_entries
|
||||
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
|
||||
WHERE (
|
||||
@search = '' OR
|
||||
runes_entries.rune ILIKE @search || '%'
|
||||
)
|
||||
ORDER BY runes_entries.number
|
||||
LIMIT @_limit OFFSET @_offset;
|
||||
|
||||
-- name: GetOngoingRuneEntries :many
|
||||
WITH states AS (
|
||||
-- select latest state
|
||||
SELECT DISTINCT ON (rune_id) * FROM runes_entry_states WHERE block_height <= @height::integer ORDER BY rune_id, block_height DESC
|
||||
)
|
||||
SELECT * FROM runes_entries
|
||||
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
|
||||
WHERE (
|
||||
runes_entries.terms = TRUE AND
|
||||
states.mints < runes_entries.terms_cap AND
|
||||
(
|
||||
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= @height::integer
|
||||
) AND (
|
||||
runes_entries.terms_height_end IS NULL OR @height::integer <= runes_entries.terms_height_end
|
||||
) AND (
|
||||
runes_entries.terms_offset_start IS NULL OR runes_entries.terms_offset_start + runes_entries.etching_block <= @height::integer
|
||||
) AND (
|
||||
runes_entries.terms_offset_end IS NULL OR @height::integer <= runes_entries.terms_offset_start + runes_entries.etching_block
|
||||
)
|
||||
|
||||
) AND (
|
||||
@search::text = '' OR
|
||||
runes_entries.rune ILIKE @search::text || '%'
|
||||
)
|
||||
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
|
||||
LIMIT @_limit OFFSET @_offset;
|
||||
|
||||
-- name: GetRuneIdFromRune :one
|
||||
SELECT rune_id FROM runes_entries WHERE rune = $1;
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ type RunesReaderDataGateway interface {
|
||||
GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (*runes.RuneEntry, error)
|
||||
// GetRuneEntryByRuneIdAndHeightBatch returns the RuneEntries for the given runeIds and block height.
|
||||
GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error)
|
||||
// GetRuneEntries returns a list of rune entries, sorted by etching order. If search is not empty, it will filter the results by rune name (prefix).
|
||||
GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
|
||||
// GetOngoingRuneEntries returns a list of ongoing rune entries (can still mint), sorted by mint progress percent. If search is not empty, it will filter the results by rune name (prefix).
|
||||
GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
|
||||
// CountRuneEntries returns the number of existing rune entries.
|
||||
CountRuneEntries(ctx context.Context) (uint64, error)
|
||||
|
||||
@@ -56,6 +60,8 @@ type RunesReaderDataGateway interface {
|
||||
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
|
||||
// GetBalancesByPkScriptAndRuneId returns the balance for the given pkScript and runeId at the given blockHeight.
|
||||
GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript []byte, runeId runes.RuneId, blockHeight uint64) (*entity.Balance, error)
|
||||
// GetTotalHoldersByRuneIds returns the total holders of each the given runeIds.
|
||||
GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error)
|
||||
}
|
||||
|
||||
type RunesWriterDataGateway interface {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"github.com/gaze-network/indexer-network/pkg/reportingclient"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Make sure to implement the Bitcoin Processor interface
|
||||
@@ -120,39 +118,34 @@ func (p *Processor) ensureValidState(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var genesisRuneId = runes.RuneId{BlockHeight: 1, TxIndex: 0}
|
||||
|
||||
func (p *Processor) ensureGenesisRune(ctx context.Context, network common.Network) error {
|
||||
_, err := p.runesDg.GetRuneEntryByRuneId(ctx, genesisRuneId)
|
||||
genesisRuneConfig, ok := constants.GenesisRuneConfigMap[network]
|
||||
if !ok {
|
||||
logger.Panic("genesis rune config not found", slogx.Stringer("network", network))
|
||||
}
|
||||
_, err := p.runesDg.GetRuneEntryByRuneId(ctx, genesisRuneConfig.RuneId)
|
||||
if err != nil && !errors.Is(err, errs.NotFound) {
|
||||
return errors.Wrap(err, "failed to get genesis rune entry")
|
||||
}
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
runeEntry := &runes.RuneEntry{
|
||||
RuneId: genesisRuneId,
|
||||
Number: 0,
|
||||
Divisibility: 0,
|
||||
Premine: uint128.Zero,
|
||||
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
|
||||
Symbol: '\u29c9',
|
||||
Terms: &runes.Terms{
|
||||
Amount: lo.ToPtr(uint128.From64(1)),
|
||||
Cap: &uint128.Max,
|
||||
HeightStart: lo.ToPtr(network.HalvingInterval() * 4),
|
||||
HeightEnd: lo.ToPtr(network.HalvingInterval() * 5),
|
||||
OffsetStart: nil,
|
||||
OffsetEnd: nil,
|
||||
},
|
||||
Turbo: true,
|
||||
RuneId: genesisRuneConfig.RuneId,
|
||||
Number: genesisRuneConfig.Number,
|
||||
Divisibility: genesisRuneConfig.Divisibility,
|
||||
Premine: genesisRuneConfig.Premine,
|
||||
SpacedRune: genesisRuneConfig.SpacedRune,
|
||||
Symbol: genesisRuneConfig.Symbol,
|
||||
Terms: genesisRuneConfig.Terms,
|
||||
Turbo: genesisRuneConfig.Turbo,
|
||||
Mints: uint128.Zero,
|
||||
BurnedAmount: uint128.Zero,
|
||||
CompletedAt: time.Time{},
|
||||
CompletedAtHeight: nil,
|
||||
EtchingBlock: 1,
|
||||
EtchingTxHash: chainhash.Hash{},
|
||||
EtchedAt: time.Time{},
|
||||
EtchingBlock: genesisRuneConfig.RuneId.BlockHeight,
|
||||
EtchingTxHash: genesisRuneConfig.EtchingTxHash,
|
||||
EtchedAt: genesisRuneConfig.EtchedAt,
|
||||
}
|
||||
if err := p.runesDg.CreateRuneEntry(ctx, runeEntry, genesisRuneId.BlockHeight); err != nil {
|
||||
if err := p.runesDg.CreateRuneEntry(ctx, runeEntry, genesisRuneConfig.RuneId.BlockHeight); err != nil {
|
||||
return errors.Wrap(err, "failed to create genesis rune entry")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
|
||||
if err != nil && errors.Is(err, errs.NotFound) && blockHeader.Height-1 == constants.StartingBlockHeader[p.network].Height {
|
||||
prevIndexedBlock = &entity.IndexedBlock{
|
||||
Height: constants.StartingBlockHeader[p.network].Height,
|
||||
Hash: constants.StartingBlockHeader[p.network].Hash,
|
||||
Hash: chainhash.Hash{},
|
||||
EventHash: chainhash.Hash{},
|
||||
CumulativeEventHash: chainhash.Hash{},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: batch.go
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: data.sql
|
||||
|
||||
package gen
|
||||
@@ -428,6 +428,118 @@ func (q *Queries) GetLatestIndexedBlock(ctx context.Context) (RunesIndexedBlock,
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOngoingRuneEntries = `-- name: GetOngoingRuneEntries :many
|
||||
WITH states AS (
|
||||
-- select latest state
|
||||
SELECT DISTINCT ON (rune_id) rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entry_states WHERE block_height <= $1::integer ORDER BY rune_id, block_height DESC
|
||||
)
|
||||
SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, etching_block, etching_tx_hash, etched_at, states.rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entries
|
||||
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
|
||||
WHERE (
|
||||
runes_entries.terms = TRUE AND
|
||||
states.mints < runes_entries.terms_cap AND
|
||||
(
|
||||
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= $1::integer
|
||||
) AND (
|
||||
runes_entries.terms_height_end IS NULL OR $1::integer <= runes_entries.terms_height_end
|
||||
) AND (
|
||||
runes_entries.terms_offset_start IS NULL OR runes_entries.terms_offset_start + runes_entries.etching_block <= $1::integer
|
||||
) AND (
|
||||
runes_entries.terms_offset_end IS NULL OR $1::integer <= runes_entries.terms_offset_start + runes_entries.etching_block
|
||||
)
|
||||
|
||||
) AND (
|
||||
$2::text = '' OR
|
||||
runes_entries.rune ILIKE $2::text || '%'
|
||||
)
|
||||
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
|
||||
LIMIT $4 OFFSET $3
|
||||
`
|
||||
|
||||
type GetOngoingRuneEntriesParams struct {
|
||||
Height int32
|
||||
Search string
|
||||
Offset int32
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type GetOngoingRuneEntriesRow struct {
|
||||
RuneID string
|
||||
Number int64
|
||||
Rune string
|
||||
Spacers int32
|
||||
Premine pgtype.Numeric
|
||||
Symbol int32
|
||||
Divisibility int16
|
||||
Terms bool
|
||||
TermsAmount pgtype.Numeric
|
||||
TermsCap pgtype.Numeric
|
||||
TermsHeightStart pgtype.Int4
|
||||
TermsHeightEnd pgtype.Int4
|
||||
TermsOffsetStart pgtype.Int4
|
||||
TermsOffsetEnd pgtype.Int4
|
||||
Turbo bool
|
||||
EtchingBlock int32
|
||||
EtchingTxHash string
|
||||
EtchedAt pgtype.Timestamp
|
||||
RuneID_2 pgtype.Text
|
||||
BlockHeight pgtype.Int4
|
||||
Mints pgtype.Numeric
|
||||
BurnedAmount pgtype.Numeric
|
||||
CompletedAt pgtype.Timestamp
|
||||
CompletedAtHeight pgtype.Int4
|
||||
}
|
||||
|
||||
func (q *Queries) GetOngoingRuneEntries(ctx context.Context, arg GetOngoingRuneEntriesParams) ([]GetOngoingRuneEntriesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getOngoingRuneEntries,
|
||||
arg.Height,
|
||||
arg.Search,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetOngoingRuneEntriesRow
|
||||
for rows.Next() {
|
||||
var i GetOngoingRuneEntriesRow
|
||||
if err := rows.Scan(
|
||||
&i.RuneID,
|
||||
&i.Number,
|
||||
&i.Rune,
|
||||
&i.Spacers,
|
||||
&i.Premine,
|
||||
&i.Symbol,
|
||||
&i.Divisibility,
|
||||
&i.Terms,
|
||||
&i.TermsAmount,
|
||||
&i.TermsCap,
|
||||
&i.TermsHeightStart,
|
||||
&i.TermsHeightEnd,
|
||||
&i.TermsOffsetStart,
|
||||
&i.TermsOffsetEnd,
|
||||
&i.Turbo,
|
||||
&i.EtchingBlock,
|
||||
&i.EtchingTxHash,
|
||||
&i.EtchedAt,
|
||||
&i.RuneID_2,
|
||||
&i.BlockHeight,
|
||||
&i.Mints,
|
||||
&i.BurnedAmount,
|
||||
&i.CompletedAt,
|
||||
&i.CompletedAtHeight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getOutPointBalancesAtOutPoint = `-- name: GetOutPointBalancesAtOutPoint :many
|
||||
SELECT rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2
|
||||
`
|
||||
@@ -465,6 +577,105 @@ func (q *Queries) GetOutPointBalancesAtOutPoint(ctx context.Context, arg GetOutP
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRuneEntries = `-- name: GetRuneEntries :many
|
||||
WITH states AS (
|
||||
-- select latest state
|
||||
SELECT DISTINCT ON (rune_id) rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entry_states WHERE block_height <= $4 ORDER BY rune_id, block_height DESC
|
||||
)
|
||||
SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, etching_block, etching_tx_hash, etched_at, states.rune_id, block_height, mints, burned_amount, completed_at, completed_at_height FROM runes_entries
|
||||
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
|
||||
WHERE (
|
||||
$1 = '' OR
|
||||
runes_entries.rune ILIKE $1 || '%'
|
||||
)
|
||||
ORDER BY runes_entries.number
|
||||
LIMIT $3 OFFSET $2
|
||||
`
|
||||
|
||||
type GetRuneEntriesParams struct {
|
||||
Search interface{}
|
||||
Offset int32
|
||||
Limit int32
|
||||
Height int32
|
||||
}
|
||||
|
||||
type GetRuneEntriesRow struct {
|
||||
RuneID string
|
||||
Number int64
|
||||
Rune string
|
||||
Spacers int32
|
||||
Premine pgtype.Numeric
|
||||
Symbol int32
|
||||
Divisibility int16
|
||||
Terms bool
|
||||
TermsAmount pgtype.Numeric
|
||||
TermsCap pgtype.Numeric
|
||||
TermsHeightStart pgtype.Int4
|
||||
TermsHeightEnd pgtype.Int4
|
||||
TermsOffsetStart pgtype.Int4
|
||||
TermsOffsetEnd pgtype.Int4
|
||||
Turbo bool
|
||||
EtchingBlock int32
|
||||
EtchingTxHash string
|
||||
EtchedAt pgtype.Timestamp
|
||||
RuneID_2 pgtype.Text
|
||||
BlockHeight pgtype.Int4
|
||||
Mints pgtype.Numeric
|
||||
BurnedAmount pgtype.Numeric
|
||||
CompletedAt pgtype.Timestamp
|
||||
CompletedAtHeight pgtype.Int4
|
||||
}
|
||||
|
||||
func (q *Queries) GetRuneEntries(ctx context.Context, arg GetRuneEntriesParams) ([]GetRuneEntriesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getRuneEntries,
|
||||
arg.Search,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
arg.Height,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetRuneEntriesRow
|
||||
for rows.Next() {
|
||||
var i GetRuneEntriesRow
|
||||
if err := rows.Scan(
|
||||
&i.RuneID,
|
||||
&i.Number,
|
||||
&i.Rune,
|
||||
&i.Spacers,
|
||||
&i.Premine,
|
||||
&i.Symbol,
|
||||
&i.Divisibility,
|
||||
&i.Terms,
|
||||
&i.TermsAmount,
|
||||
&i.TermsCap,
|
||||
&i.TermsHeightStart,
|
||||
&i.TermsHeightEnd,
|
||||
&i.TermsOffsetStart,
|
||||
&i.TermsOffsetEnd,
|
||||
&i.Turbo,
|
||||
&i.EtchingBlock,
|
||||
&i.EtchingTxHash,
|
||||
&i.EtchedAt,
|
||||
&i.RuneID_2,
|
||||
&i.BlockHeight,
|
||||
&i.Mints,
|
||||
&i.BurnedAmount,
|
||||
&i.CompletedAt,
|
||||
&i.CompletedAtHeight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRuneEntriesByRuneIds = `-- name: GetRuneEntriesByRuneIds :many
|
||||
WITH states AS (
|
||||
-- select latest state
|
||||
@@ -971,6 +1182,43 @@ func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetR
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTotalHoldersByRuneIds = `-- name: GetTotalHoldersByRuneIds :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (rune_id, pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = ANY($1::TEXT[]) AND block_height <= $2 ORDER BY rune_id, pkscript, block_height DESC
|
||||
)
|
||||
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id
|
||||
`
|
||||
|
||||
type GetTotalHoldersByRuneIdsParams struct {
|
||||
RuneIds []string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
type GetTotalHoldersByRuneIdsRow struct {
|
||||
RuneID string
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetTotalHoldersByRuneIds(ctx context.Context, arg GetTotalHoldersByRuneIdsParams) ([]GetTotalHoldersByRuneIdsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTotalHoldersByRuneIds, arg.RuneIds, arg.BlockHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTotalHoldersByRuneIdsRow
|
||||
for rows.Next() {
|
||||
var i GetTotalHoldersByRuneIdsRow
|
||||
if err := rows.Scan(&i.RuneID, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const spendOutPointBalances = `-- name: SpendOutPointBalances :exec
|
||||
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
|
||||
`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: info.sql
|
||||
|
||||
package gen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func mapIndexerStateTypeToParams(src entity.IndexerState) gen.SetIndexerStatePar
|
||||
}
|
||||
}
|
||||
|
||||
func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntry, error) {
|
||||
func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error) {
|
||||
runeId, err := runes.NewRuneIdFromString(src.RuneID)
|
||||
if err != nil {
|
||||
return runes.RuneEntry{}, errors.Wrap(err, "failed to parse rune id")
|
||||
@@ -221,7 +221,7 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.Crea
|
||||
}
|
||||
}
|
||||
}
|
||||
etchedAt := pgtype.Timestamp{Time: time.Time{}, Valid: true}
|
||||
etchedAt := pgtype.Timestamp{Time: src.EtchedAt, Valid: true}
|
||||
|
||||
return gen.CreateRuneEntryParams{
|
||||
RuneID: runeId,
|
||||
|
||||
@@ -122,8 +122,11 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
|
||||
|
||||
func (r *Repository) GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error) {
|
||||
row, err := r.queries.GetRuneTransaction(ctx, txHash.String())
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errors.WithStack(errs.NotFound)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errors.WithStack(errs.NotFound)
|
||||
}
|
||||
return nil, errors.Wrap(err, "error during query")
|
||||
}
|
||||
|
||||
runeTxModel, runestoneModel, err := extractModelRuneTxAndRunestone(gen.GetRuneTransactionsRow(row))
|
||||
@@ -262,7 +265,7 @@ func (r *Repository) GetRuneEntryByRuneIdBatch(ctx context.Context, runeIds []ru
|
||||
runeEntries := make(map[runes.RuneId]*runes.RuneEntry, len(rows))
|
||||
var errs []error
|
||||
for i, runeEntryModel := range rows {
|
||||
runeEntry, err := mapRuneEntryModelToType(runeEntryModel)
|
||||
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(runeEntryModel))
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
|
||||
continue
|
||||
@@ -302,7 +305,7 @@ func (r *Repository) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, run
|
||||
runeEntries := make(map[runes.RuneId]*runes.RuneEntry, len(rows))
|
||||
var errs []error
|
||||
for i, runeEntryModel := range rows {
|
||||
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesByRuneIdsRow(runeEntryModel))
|
||||
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(runeEntryModel))
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
|
||||
continue
|
||||
@@ -316,6 +319,62 @@ func (r *Repository) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, run
|
||||
return runeEntries, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error) {
|
||||
rows, err := r.queries.GetRuneEntries(ctx, gen.GetRuneEntriesParams{
|
||||
Search: search,
|
||||
Height: int32(blockHeight),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during query")
|
||||
}
|
||||
|
||||
runeEntries := make([]*runes.RuneEntry, 0, len(rows))
|
||||
var errs []error
|
||||
for i, model := range rows {
|
||||
runeEntry, err := mapRuneEntryModelToType(model)
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
|
||||
continue
|
||||
}
|
||||
runeEntries = append(runeEntries, &runeEntry)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
|
||||
return runeEntries, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error) {
|
||||
rows, err := r.queries.GetOngoingRuneEntries(ctx, gen.GetOngoingRuneEntriesParams{
|
||||
Search: search,
|
||||
Height: int32(blockHeight),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during query")
|
||||
}
|
||||
|
||||
runeEntries := make([]*runes.RuneEntry, 0, len(rows))
|
||||
var errs []error
|
||||
for i, model := range rows {
|
||||
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesRow(model))
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
|
||||
continue
|
||||
}
|
||||
runeEntries = append(runeEntries, &runeEntry)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
|
||||
return runeEntries, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CountRuneEntries(ctx context.Context) (uint64, error) {
|
||||
count, err := r.queries.CountRuneEntries(ctx)
|
||||
if err != nil {
|
||||
@@ -400,6 +459,25 @@ func (r *Repository) GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error) {
|
||||
rows, err := r.queries.GetTotalHoldersByRuneIds(ctx, gen.GetTotalHoldersByRuneIdsParams{
|
||||
RuneIds: lo.Map(runeIds, func(runeId runes.RuneId, _ int) string { return runeId.String() }),
|
||||
BlockHeight: int32(blockHeight),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during query")
|
||||
}
|
||||
holders := make(map[runes.RuneId]int64, len(rows))
|
||||
for _, row := range rows {
|
||||
runeId, err := runes.NewRuneIdFromString(row.RuneID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse RuneId")
|
||||
}
|
||||
holders[runeId] = row.Count
|
||||
}
|
||||
return holders, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error {
|
||||
if tx == nil {
|
||||
return nil
|
||||
|
||||
@@ -25,3 +25,11 @@ func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId,
|
||||
}
|
||||
return balances, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error) {
|
||||
holders, err := u.runesDg.GetTotalHoldersByRuneIds(ctx, runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get total holders by rune ids")
|
||||
}
|
||||
return holders, nil
|
||||
}
|
||||
|
||||
@@ -46,3 +46,19 @@ func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeId
|
||||
}
|
||||
return runeEntry, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
|
||||
entries, err := u.runesDg.GetRuneEntries(ctx, search, blockHeight, limit, offset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to listing rune entries")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
|
||||
entries, err := u.runesDg.GetOngoingRuneEntries(ctx, search, blockHeight, limit, offset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to listing rune entries")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
@@ -16,3 +17,11 @@ func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, rune
|
||||
}
|
||||
return txs, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetRuneTransaction(ctx context.Context, hash chainhash.Hash) (*entity.RuneTransaction, error) {
|
||||
tx, err := u.runesDg.GetRuneTransaction(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetRuneTransaction")
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ func VerifySignature(address string, message string, sigBase64 string, defaultNe
|
||||
return nil
|
||||
}
|
||||
|
||||
func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.TxOut, inputIndex int) (*wire.MsgTx, error) {
|
||||
type SignTxInputOptions struct {
|
||||
SigHashType txscript.SigHashType
|
||||
}
|
||||
|
||||
func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.TxOut, inputIndex int, options ...SignTxInputOptions) (*wire.MsgTx, error) {
|
||||
if privateKey == nil {
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "PrivateKey is required")
|
||||
}
|
||||
@@ -35,6 +39,14 @@ func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.T
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "PrevTxOut is required")
|
||||
}
|
||||
|
||||
// defaults sigHashType to SigHashAll | SigHashAnyOneCanPay
|
||||
if len(options) == 0 {
|
||||
options = append(options, SignTxInputOptions{
|
||||
SigHashType: txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
|
||||
})
|
||||
}
|
||||
sigHashType := options[0].SigHashType
|
||||
|
||||
prevOutFetcher := txscript.NewCannedPrevOutputFetcher(prevTxOut.PkScript, prevTxOut.Value)
|
||||
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
|
||||
if len(tx.TxIn) <= inputIndex {
|
||||
@@ -53,7 +65,7 @@ func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.T
|
||||
inputIndex,
|
||||
prevTxOut.Value,
|
||||
prevTxOut.PkScript,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
sigHashType,
|
||||
privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sign")
|
||||
@@ -66,7 +78,7 @@ func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.T
|
||||
inputIndex,
|
||||
prevTxOut.Value,
|
||||
prevTxOut.PkScript,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
sigHashType,
|
||||
privateKey,
|
||||
true,
|
||||
)
|
||||
@@ -79,7 +91,7 @@ func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.T
|
||||
tx,
|
||||
inputIndex,
|
||||
prevTxOut.PkScript,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
sigHashType,
|
||||
privateKey,
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -163,6 +163,7 @@ func TestSignTxInput(t *testing.T) {
|
||||
AddOp(txscript.OP_0).
|
||||
AddData(pubKeyHash).
|
||||
Script()
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
|
||||
signedTx, err := SignTxInput(
|
||||
@@ -175,7 +176,9 @@ func TestSignTxInput(t *testing.T) {
|
||||
pubKey := privKey.PubKey()
|
||||
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||
address, err := btcutil.NewAddressPubKeyHash(pubKeyHash, &chaincfg.MainNetParams)
|
||||
require.NoError(t, err)
|
||||
pkScript, err := txscript.PayToAddrScript(address)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
|
||||
signedTx, err := SignTxInput(
|
||||
@@ -184,4 +187,61 @@ func TestSignTxInput(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
verifySignedTx(t, signedTx, prevTxOut)
|
||||
})
|
||||
t.Run("custom sighash type", func(t *testing.T) {
|
||||
pubKey := privKey.PubKey()
|
||||
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||
|
||||
pkScript, err := txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_0).
|
||||
AddData(pubKeyHash).
|
||||
Script()
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
|
||||
|
||||
sigHashTypes := []txscript.SigHashType{
|
||||
txscript.SigHashAll,
|
||||
txscript.SigHashNone,
|
||||
txscript.SigHashSingle,
|
||||
txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
|
||||
txscript.SigHashNone | txscript.SigHashAnyOneCanPay,
|
||||
txscript.SigHashSingle | txscript.SigHashAnyOneCanPay,
|
||||
}
|
||||
for _, sigHashType := range sigHashTypes {
|
||||
signedTx, err := SignTxInput(
|
||||
tx, privKey, prevTxOut, 0, SignTxInputOptions{
|
||||
SigHashType: sigHashType,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
verifySignedTx(t, signedTx, prevTxOut)
|
||||
|
||||
// check last byte of signature equals to sigHashType
|
||||
signature := signedTx.TxIn[0].Witness[0]
|
||||
assert.Equal(t, sigHashType, txscript.SigHashType(signature[len(signature)-1]))
|
||||
}
|
||||
})
|
||||
t.Run("default sighash type", func(t *testing.T) {
|
||||
pubKey := privKey.PubKey()
|
||||
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||
|
||||
pkScript, err := txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_0).
|
||||
AddData(pubKeyHash).
|
||||
Script()
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
|
||||
|
||||
signedTx, err := SignTxInput(
|
||||
tx, privKey, prevTxOut, 0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
verifySignedTx(t, signedTx, prevTxOut)
|
||||
|
||||
// check last byte of signature equals to sigHashType
|
||||
signature := signedTx.TxIn[0].Witness[0]
|
||||
expected := txscript.SigHashAll | txscript.SigHashAnyOneCanPay
|
||||
assert.Equal(t, expected, txscript.SigHashType(signature[len(signature)-1]))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user