Compare commits

..

26 Commits

Author SHA1 Message Date
gazenw
e9ce8df01a Merge pull request #67 from gaze-network/develop
Release v0.5.5
2024-10-11 14:25:05 +07:00
Gaze
3ff73a99f8 Merge branch 'main' into develop 2024-10-11 14:24:36 +07:00
gazenw
96afdfd255 fix: order get ongoing tokens by mint transactions (#66) 2024-10-11 14:23:31 +07:00
gazenw
c49e39be97 feat: optimize flush speed for Runes (#65)
* feat: switch to batchexec for faster inserts

* feat: add time taken log

* feat: add process time log

* feat: add event logs
2024-10-11 14:21:26 +07:00
Gaze
12985ae432 feat: remove conns timeout 2024-10-08 01:14:24 +07:00
Gaze
2d51e52b83 feat: support to config 2024-10-08 01:12:49 +07:00
Gaze
618220d0cb feat: support high httpclient conns 2024-10-08 00:44:24 +07:00
gazenw
6004744721 Merge pull request #64 from gaze-network/develop
Release v0.5.1
2024-10-07 21:18:11 +07:00
gazenw
90ed7bc350 fix: update sql (#63) 2024-10-07 21:17:16 +07:00
gazenw
7a0fe84e40 Merge pull request #62 from gaze-network/develop
Release v0.5.0
2024-10-06 23:52:10 +07:00
gazenw
f1d4651042 feat(runes): add Get Transaction by hash api (#39)
* feat: implement pagination on get balance, get holders

* feat: paginate get transactions

* fix: remove debug

* feat: implement pagination in get utxos

* feat: sort response in get holders

* feat: cap batch query

* feat: add default limits to all endpoints

* chore: rename endpoint funcs

* fix: parse rune name spacers

* feat(runes): get tx by hash api

* fix: error

* refactor: use map to collect rune ids

---------

Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-10-06 23:50:13 +07:00
gazenw
5f4f50a9e5 Merge pull request #61 from gaze-network/develop
Release v0.4.7
2024-10-06 21:00:57 +07:00
gazenw
32c3c5c1d4 fix: correctly insert etchedAt in rune entry (#60) 2024-10-06 20:55:38 +07:00
Gaze
2a572e6d1e fix: migration 2024-10-06 20:29:25 +07:00
Gaze
aa25a6882b Merge remote-tracking branch 'origin/main' into develop 2024-10-06 20:06:06 +07:00
gazenw
6182c63150 Merge pull request #59 from gaze-network/develop
Release v0.4.5
2024-10-06 19:59:35 +07:00
gazenw
e1f8eaa3e1 fix: unescape query id (#58) 2024-10-06 19:59:06 +07:00
gazenw
107836ae39 feat(runes): add Get Tokens API (#38)
* feat: implement pagination on get balance, get holders

* feat: paginate get transactions

* fix: remove debug

* feat: implement pagination in get utxos

* feat: sort response in get holders

* feat: cap batch query

* feat: add default limits to all endpoints

* chore: rename endpoint funcs

* fix: parse rune name spacers

* feat(runes): add get token list api

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

* feat: remove unused code

* fix: count holders distinct pkscript

* feat: implement additional scopes

* chore: comments

* feat: implement search

* refactor: switch to use paginationRequest

* refactor: rename get token list to get tokens

* fix: count total holders by rune ids

* fix: rename file

* fix: rename minting to ongoing

* fix: get ongoing check rune is mintable

* chore: disable gosec g115

* fix: pr

---------

Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-10-06 19:30:57 +07:00
Gaze
1bd84b0154 fix: bump sqlc verify action version to 1.27.0 2024-10-06 15:41:10 +07:00
Gaze
de26a4c21d feat(errs): add retryable error 2024-10-06 11:12:51 +07:00
Gaze
1dc57d74e0 Merge remote-tracking branch 'origin/main' into develop 2024-10-05 01:38:34 +07:00
gazenw
7c0e28d8ea Merge pull request #57 from gaze-network/develop
Release 0.4.4
2024-10-05 01:37:50 +07:00
gazenw
754fd1e997 fix: only check for chain reorg if current block has hash (#56)
* fix: only check for chain reorg if current block has hash

* fix: remove starting block hash

* fix: don't use starting block hash
2024-10-05 01:35:04 +07:00
Gaze
66f03f7107 feat: allow custom sigHashType when signing 2024-10-04 23:05:48 +07:00
gazenw
7a863987ec Merge pull request #55 from gaze-network/develop
Release v0.4.3
2024-10-04 13:23:30 +07:00
gazenw
f9c6ef8dfd fix: add different genesis runes config for each network (#54)
* fix: add different genesis runes config for each network

* fix: use slogx.Stringer

* refactor: remove unused value
2024-10-04 13:22:53 +07:00
40 changed files with 1636 additions and 408 deletions

View File

@@ -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

View File

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

View File

@@ -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")

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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{

View File

@@ -0,0 +1,172 @@
package httphandler
import (
"fmt"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
const (
getTokensMaxLimit = 1000
)
type GetTokensScope string
const (
GetTokensScopeAll GetTokensScope = "all"
GetTokensScopeOngoing GetTokensScope = "ongoing"
)
func (s GetTokensScope) IsValid() bool {
switch s {
case GetTokensScopeAll, GetTokensScopeOngoing:
return true
}
return false
}
type getTokensRequest struct {
paginationRequest
Search string `query:"search"`
BlockHeight uint64 `query:"blockHeight"`
Scope GetTokensScope `query:"scope"`
}
func (req getTokensRequest) Validate() error {
var errList []error
if err := req.paginationRequest.Validate(); err != nil {
errList = append(errList, err)
}
if req.Limit > getTokensMaxLimit {
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
}
if req.Scope != "" && !req.Scope.IsValid() {
errList = append(errList, errors.Errorf("invalid scope: %s", req.Scope))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
func (req *getTokensRequest) ParseDefault() error {
if err := req.paginationRequest.ParseDefault(); err != nil {
return errors.WithStack(err)
}
if req.Scope == "" {
req.Scope = GetTokensScopeAll
}
return nil
}
type getTokensResult struct {
List []getTokenInfoResult `json:"list"`
}
type getTokensResponse = HttpResponse[getTokensResult]
func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
var req getTokensRequest
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
if err := req.ParseDefault(); err != nil {
return errors.WithStack(err)
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
// remove spacers
search := strings.Replace(strings.Replace(req.Search, "•", "", -1), ".", "", -1)
var entries []*runes.RuneEntry
switch req.Scope {
case GetTokensScopeAll:
entries, err = h.usecase.GetRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryList")
}
case GetTokensScopeOngoing:
entries, err = h.usecase.GetOngoingRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryList")
}
default:
return errs.NewPublicError(fmt.Sprintf("invalid scope: %s", req.Scope))
}
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
totalHolders, err := h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
}
result := make([]getTokenInfoResult, 0, len(entries))
for _, ent := range entries {
totalSupply, err := ent.Supply()
if err != nil {
return errors.Wrap(err, "cannot get total supply of rune")
}
mintedAmount, err := ent.MintedAmount()
if err != nil {
return errors.Wrap(err, "cannot get minted amount of rune")
}
circulatingSupply := mintedAmount.Sub(ent.BurnedAmount)
terms := lo.FromPtr(ent.Terms)
result = append(result, getTokenInfoResult{
Id: ent.RuneId,
Name: ent.SpacedRune,
Symbol: string(ent.Symbol),
TotalSupply: totalSupply,
CirculatingSupply: circulatingSupply,
MintedAmount: mintedAmount,
BurnedAmount: ent.BurnedAmount,
Decimals: ent.Divisibility,
DeployedAt: ent.EtchedAt.Unix(),
DeployedAtHeight: ent.EtchingBlock,
CompletedAt: lo.Ternary(ent.CompletedAt.IsZero(), nil, lo.ToPtr(ent.CompletedAt.Unix())),
CompletedAtHeight: ent.CompletedAtHeight,
HoldersCount: int(totalHolders[ent.RuneId]),
Extend: tokenInfoExtend{
Entry: entry{
Divisibility: ent.Divisibility,
Premine: ent.Premine,
Rune: ent.SpacedRune.Rune,
Spacers: ent.SpacedRune.Spacers,
Symbol: string(ent.Symbol),
Terms: entryTerms{
Amount: lo.FromPtr(terms.Amount),
Cap: lo.FromPtr(terms.Cap),
HeightStart: terms.HeightStart,
HeightEnd: terms.HeightEnd,
OffsetStart: terms.OffsetStart,
OffsetEnd: terms.OffsetEnd,
},
Turbo: ent.Turbo,
},
},
})
}
return errors.WithStack(ctx.JSON(getTokensResponse{
Result: &getTokensResult{
List: result,
},
}))
}

View File

@@ -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,
}))
}

View File

@@ -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")

View File

@@ -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())

View File

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

View File

@@ -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
}

View File

@@ -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),
},
}

View File

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

View File

@@ -13,6 +13,12 @@ SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3
-- name: GetBalanceByPkScriptAndRuneId :one
SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_height <= $3 ORDER BY block_height DESC LIMIT 1;
-- name: GetTotalHoldersByRuneIds :many
WITH balances AS (
SELECT DISTINCT ON (rune_id, pkscript) * FROM runes_balances WHERE rune_id = ANY(@rune_ids::TEXT[]) AND block_height <= @block_height ORDER BY rune_id, pkscript, block_height DESC
)
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id;
-- name: GetOutPointBalancesAtOutPoint :many
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
@@ -57,6 +63,49 @@ 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
COALESCE(runes_entries.terms_amount, 0) != 0 AND
COALESCE(runes_entries.terms_cap, 0) != 0 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 DESC
LIMIT @_limit OFFSET @_offset;
-- name: GetRuneIdFromRune :one
SELECT rune_id FROM runes_entries WHERE rune = $1;
@@ -87,24 +136,24 @@ SELECT * FROM runes_transactions
-- name: CountRuneEntries :one
SELECT COUNT(*) FROM runes_entries;
-- name: CreateRuneEntry :exec
-- name: CreateRuneEntries :batchexec
INSERT INTO runes_entries (rune_id, rune, number, 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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18);
-- name: CreateRuneEntryState :exec
-- name: CreateRuneEntryStates :batchexec
INSERT INTO runes_entry_states (rune_id, block_height, mints, burned_amount, completed_at, completed_at_height) VALUES ($1, $2, $3, $4, $5, $6);
-- name: CreateRuneTransaction :exec
-- name: CreateRuneTransactions :batchexec
INSERT INTO runes_transactions (hash, block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
-- name: CreateRunestone :exec
-- name: CreateRunestones :batchexec
INSERT INTO runes_runestones (tx_hash, block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21);
-- name: CreateOutPointBalances :batchexec
INSERT INTO runes_outpoint_balances (rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height) VALUES ($1, $2, $3, $4, $5, $6, $7);
-- name: SpendOutPointBalances :exec
-- name: SpendOutPointBalancesBatch :batchexec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3;
-- name: CreateRuneBalanceAtBlock :batchexec

View File

@@ -44,6 +44,10 @@ type RunesReaderDataGateway interface {
GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (*runes.RuneEntry, error)
// GetRuneEntryByRuneIdAndHeightBatch returns the RuneEntries for the given runeIds and block height.
GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error)
// GetRuneEntries returns a list of rune entries, sorted by etching order. If search is not empty, it will filter the results by rune name (prefix).
GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
// GetOngoingRuneEntries returns a list of ongoing rune entries (can still mint), sorted by mint progress percent. If search is not empty, it will filter the results by rune name (prefix).
GetOngoingRuneEntries(ctx context.Context, search string, blockHeight uint64, limit int32, offset int32) ([]*runes.RuneEntry, error)
// CountRuneEntries returns the number of existing rune entries.
CountRuneEntries(ctx context.Context) (uint64, error)
@@ -56,15 +60,17 @@ 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 {
CreateRuneEntry(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error
CreateRuneEntryState(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error
CreateRuneEntries(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error
CreateRuneEntryStates(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error
CreateOutPointBalances(ctx context.Context, outPointBalances []*entity.OutPointBalance) error
SpendOutPointBalances(ctx context.Context, outPoint wire.OutPoint, blockHeight uint64) error
SpendOutPointBalancesBatch(ctx context.Context, outPoints []wire.OutPoint, blockHeight uint64) error
CreateRuneBalances(ctx context.Context, params []CreateRuneBalancesParams) error
CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error
CreateRuneTransactions(ctx context.Context, txs []*entity.RuneTransaction) error
CreateIndexedBlock(ctx context.Context, block *entity.IndexedBlock) error
// TODO: collapse these into a single function (ResetStateToHeight)?

View File

@@ -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,37 @@ 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.CreateRuneEntries(ctx, []*runes.RuneEntry{runeEntry}, genesisRuneConfig.RuneId.BlockHeight); err != nil {
return errors.Wrap(err, "failed to create genesis rune entry")
}
if err := p.runesDg.CreateRuneEntryStates(ctx, []*runes.RuneEntry{runeEntry}, genesisRuneConfig.RuneId.BlockHeight); err != nil {
return errors.Wrap(err, "failed to create genesis rune entry")
}
}

View File

@@ -27,19 +27,26 @@ import (
func (p *Processor) Process(ctx context.Context, blocks []*types.Block) error {
for _, block := range blocks {
ctx := logger.WithContext(ctx, slog.Int64("height", block.Header.Height))
logger.DebugContext(ctx, "Processing new block", slog.Int("txs", len(block.Transactions)))
logger.InfoContext(ctx, "Processing new block",
slogx.String("event", "runes_processor_processing_block"),
slog.Int("txs", len(block.Transactions)),
)
start := time.Now()
for _, tx := range block.Transactions {
if err := p.processTx(ctx, tx, block.Header); err != nil {
return errors.Wrap(err, "failed to process tx")
}
}
timeTakenToProcess := time.Since(start)
logger.InfoContext(ctx, "Processed block",
slogx.String("event", "runes_processor_processed_block"),
slog.Duration("time_taken", timeTakenToProcess),
)
if err := p.flushBlock(ctx, block.Header); err != nil {
return errors.Wrap(err, "failed to flush block")
}
logger.DebugContext(ctx, "Inserted new block")
}
return nil
}
@@ -669,6 +676,7 @@ func (p *Processor) getRunesBalancesAtOutPoint(ctx context.Context, outPoint wir
}
func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeader) error {
start := time.Now()
runesDgTx, err := p.runesDg.BeginRunesTx(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin runes tx")
@@ -691,7 +699,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{},
}
@@ -715,78 +723,82 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
return errors.Wrap(err, "failed to create indexed block")
}
// flush new rune entries
{
for _, runeEntry := range p.newRuneEntries {
if err := runesDgTx.CreateRuneEntry(ctx, runeEntry, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create rune entry")
}
}
p.newRuneEntries = make(map[runes.RuneId]*runes.RuneEntry)
newRuneEntries := lo.Values(p.newRuneEntries)
if err := runesDgTx.CreateRuneEntries(ctx, newRuneEntries, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create rune entry")
}
p.newRuneEntries = make(map[runes.RuneId]*runes.RuneEntry)
// flush new rune entry states
{
for _, runeEntry := range p.newRuneEntryStates {
if err := runesDgTx.CreateRuneEntryState(ctx, runeEntry, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create rune entry state")
}
}
p.newRuneEntryStates = make(map[runes.RuneId]*runes.RuneEntry)
newRuneEntryStates := lo.Values(p.newRuneEntryStates)
if err := runesDgTx.CreateRuneEntryStates(ctx, newRuneEntryStates, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create rune entry state")
}
p.newRuneEntryStates = make(map[runes.RuneId]*runes.RuneEntry)
// flush new outpoint balances
{
newBalances := make([]*entity.OutPointBalance, 0)
for _, balances := range p.newOutPointBalances {
newBalances = append(newBalances, balances...)
}
if err := runesDgTx.CreateOutPointBalances(ctx, newBalances); err != nil {
return errors.Wrap(err, "failed to create outpoint balances")
}
p.newOutPointBalances = make(map[wire.OutPoint][]*entity.OutPointBalance)
newBalances := make([]*entity.OutPointBalance, 0)
for _, balances := range p.newOutPointBalances {
newBalances = append(newBalances, balances...)
}
if err := runesDgTx.CreateOutPointBalances(ctx, newBalances); err != nil {
return errors.Wrap(err, "failed to create outpoint balances")
}
p.newOutPointBalances = make(map[wire.OutPoint][]*entity.OutPointBalance)
// flush new spend outpoints
{
for _, outPoint := range p.newSpendOutPoints {
if err := runesDgTx.SpendOutPointBalances(ctx, outPoint, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create spend outpoint")
}
}
p.newSpendOutPoints = make([]wire.OutPoint, 0)
newSpendOutPoints := p.newSpendOutPoints
if err := runesDgTx.SpendOutPointBalancesBatch(ctx, newSpendOutPoints, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create spend outpoint")
}
p.newSpendOutPoints = make([]wire.OutPoint, 0)
// flush new balances
{
params := make([]datagateway.CreateRuneBalancesParams, 0)
for pkScriptStr, balances := range p.newBalances {
pkScript, err := hex.DecodeString(pkScriptStr)
if err != nil {
return errors.Wrap(err, "failed to decode pk script")
}
for runeId, balance := range balances {
params = append(params, datagateway.CreateRuneBalancesParams{
PkScript: pkScript,
RuneId: runeId,
Balance: balance,
BlockHeight: uint64(blockHeader.Height),
})
}
params := make([]datagateway.CreateRuneBalancesParams, 0)
for pkScriptStr, balances := range p.newBalances {
pkScript, err := hex.DecodeString(pkScriptStr)
if err != nil {
return errors.Wrap(err, "failed to decode pk script")
}
if err := runesDgTx.CreateRuneBalances(ctx, params); err != nil {
return errors.Wrap(err, "failed to create balances at block")
for runeId, balance := range balances {
params = append(params, datagateway.CreateRuneBalancesParams{
PkScript: pkScript,
RuneId: runeId,
Balance: balance,
BlockHeight: uint64(blockHeader.Height),
})
}
p.newBalances = make(map[string]map[runes.RuneId]uint128.Uint128)
}
if err := runesDgTx.CreateRuneBalances(ctx, params); err != nil {
return errors.Wrap(err, "failed to create balances at block")
}
p.newBalances = make(map[string]map[runes.RuneId]uint128.Uint128)
// flush new rune transactions
{
for _, runeTx := range p.newRuneTxs {
if err := runesDgTx.CreateRuneTransaction(ctx, runeTx); err != nil {
return errors.Wrap(err, "failed to create rune transaction")
}
}
p.newRuneTxs = make([]*entity.RuneTransaction, 0)
newRuneTxs := p.newRuneTxs
if err := runesDgTx.CreateRuneTransactions(ctx, newRuneTxs); err != nil {
return errors.Wrap(err, "failed to create rune transaction")
}
p.newRuneTxs = make([]*entity.RuneTransaction, 0)
if err := runesDgTx.Commit(ctx); err != nil {
return errors.Wrap(err, "failed to commit runes tx")
}
timeTaken := time.Since(start)
logger.InfoContext(ctx, "Flushed block",
slogx.String("event", "runes_processor_flushed_block"),
slog.Int64("height", blockHeader.Height),
slog.String("hash", blockHeader.Hash.String()),
slog.String("event_hash", hex.EncodeToString(eventHash[:])),
slog.String("cumulative_event_hash", hex.EncodeToString(cumulativeEventHash[:])),
slog.Int("new_rune_entries", len(newRuneEntries)),
slog.Int("new_rune_entry_states", len(newRuneEntryStates)),
slog.Int("new_outpoint_balances", len(newBalances)),
slog.Int("new_spend_outpoints", len(newSpendOutPoints)),
slog.Int("new_balances", len(params)),
slog.Int("new_rune_txs", len(newRuneTxs)),
slogx.Duration("time_taken", timeTaken),
)
// submit event to reporting system
if p.reportingClient != nil {

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: batch.go
package gen
@@ -128,3 +128,344 @@ func (b *CreateRuneBalanceAtBlockBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createRuneEntries = `-- name: CreateRuneEntries :batchexec
INSERT INTO runes_entries (rune_id, rune, number, 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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`
type CreateRuneEntriesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateRuneEntriesParams struct {
RuneID string
Rune string
Number int64
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
}
func (q *Queries) CreateRuneEntries(ctx context.Context, arg []CreateRuneEntriesParams) *CreateRuneEntriesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.RuneID,
a.Rune,
a.Number,
a.Spacers,
a.Premine,
a.Symbol,
a.Divisibility,
a.Terms,
a.TermsAmount,
a.TermsCap,
a.TermsHeightStart,
a.TermsHeightEnd,
a.TermsOffsetStart,
a.TermsOffsetEnd,
a.Turbo,
a.EtchingBlock,
a.EtchingTxHash,
a.EtchedAt,
}
batch.Queue(createRuneEntries, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateRuneEntriesBatchResults{br, len(arg), false}
}
func (b *CreateRuneEntriesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateRuneEntriesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createRuneEntryStates = `-- name: CreateRuneEntryStates :batchexec
INSERT INTO runes_entry_states (rune_id, block_height, mints, burned_amount, completed_at, completed_at_height) VALUES ($1, $2, $3, $4, $5, $6)
`
type CreateRuneEntryStatesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateRuneEntryStatesParams struct {
RuneID string
BlockHeight int32
Mints pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) CreateRuneEntryStates(ctx context.Context, arg []CreateRuneEntryStatesParams) *CreateRuneEntryStatesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.RuneID,
a.BlockHeight,
a.Mints,
a.BurnedAmount,
a.CompletedAt,
a.CompletedAtHeight,
}
batch.Queue(createRuneEntryStates, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateRuneEntryStatesBatchResults{br, len(arg), false}
}
func (b *CreateRuneEntryStatesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateRuneEntryStatesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createRuneTransactions = `-- name: CreateRuneTransactions :batchexec
INSERT INTO runes_transactions (hash, block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type CreateRuneTransactionsBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateRuneTransactionsParams struct {
Hash string
BlockHeight int32
Index int32
Timestamp pgtype.Timestamp
Inputs []byte
Outputs []byte
Mints []byte
Burns []byte
RuneEtched bool
}
func (q *Queries) CreateRuneTransactions(ctx context.Context, arg []CreateRuneTransactionsParams) *CreateRuneTransactionsBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Hash,
a.BlockHeight,
a.Index,
a.Timestamp,
a.Inputs,
a.Outputs,
a.Mints,
a.Burns,
a.RuneEtched,
}
batch.Queue(createRuneTransactions, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateRuneTransactionsBatchResults{br, len(arg), false}
}
func (b *CreateRuneTransactionsBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateRuneTransactionsBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createRunestones = `-- name: CreateRunestones :batchexec
INSERT INTO runes_runestones (tx_hash, block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
`
type CreateRunestonesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateRunestonesParams struct {
TxHash string
BlockHeight int32
Etching bool
EtchingDivisibility pgtype.Int2
EtchingPremine pgtype.Numeric
EtchingRune pgtype.Text
EtchingSpacers pgtype.Int4
EtchingSymbol pgtype.Int4
EtchingTerms pgtype.Bool
EtchingTermsAmount pgtype.Numeric
EtchingTermsCap pgtype.Numeric
EtchingTermsHeightStart pgtype.Int4
EtchingTermsHeightEnd pgtype.Int4
EtchingTermsOffsetStart pgtype.Int4
EtchingTermsOffsetEnd pgtype.Int4
EtchingTurbo pgtype.Bool
Edicts []byte
Mint pgtype.Text
Pointer pgtype.Int4
Cenotaph bool
Flaws int32
}
func (q *Queries) CreateRunestones(ctx context.Context, arg []CreateRunestonesParams) *CreateRunestonesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.TxHash,
a.BlockHeight,
a.Etching,
a.EtchingDivisibility,
a.EtchingPremine,
a.EtchingRune,
a.EtchingSpacers,
a.EtchingSymbol,
a.EtchingTerms,
a.EtchingTermsAmount,
a.EtchingTermsCap,
a.EtchingTermsHeightStart,
a.EtchingTermsHeightEnd,
a.EtchingTermsOffsetStart,
a.EtchingTermsOffsetEnd,
a.EtchingTurbo,
a.Edicts,
a.Mint,
a.Pointer,
a.Cenotaph,
a.Flaws,
}
batch.Queue(createRunestones, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateRunestonesBatchResults{br, len(arg), false}
}
func (b *CreateRunestonesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateRunestonesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const spendOutPointBalancesBatch = `-- name: SpendOutPointBalancesBatch :batchexec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
`
type SpendOutPointBalancesBatchBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type SpendOutPointBalancesBatchParams struct {
SpentHeight pgtype.Int4
TxHash string
TxIdx int32
}
func (q *Queries) SpendOutPointBalancesBatch(ctx context.Context, arg []SpendOutPointBalancesBatchParams) *SpendOutPointBalancesBatchBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.SpentHeight,
a.TxHash,
a.TxIdx,
}
batch.Queue(spendOutPointBalancesBatch, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &SpendOutPointBalancesBatchBatchResults{br, len(arg), false}
}
func (b *SpendOutPointBalancesBatchBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *SpendOutPointBalancesBatchBatchResults) Close() error {
b.closed = true
return b.br.Close()
}

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
// source: data.sql
package gen
@@ -45,168 +45,6 @@ func (q *Queries) CreateIndexedBlock(ctx context.Context, arg CreateIndexedBlock
return err
}
const createRuneEntry = `-- name: CreateRuneEntry :exec
INSERT INTO runes_entries (rune_id, rune, number, 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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`
type CreateRuneEntryParams struct {
RuneID string
Rune string
Number int64
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
}
func (q *Queries) CreateRuneEntry(ctx context.Context, arg CreateRuneEntryParams) error {
_, err := q.db.Exec(ctx, createRuneEntry,
arg.RuneID,
arg.Rune,
arg.Number,
arg.Spacers,
arg.Premine,
arg.Symbol,
arg.Divisibility,
arg.Terms,
arg.TermsAmount,
arg.TermsCap,
arg.TermsHeightStart,
arg.TermsHeightEnd,
arg.TermsOffsetStart,
arg.TermsOffsetEnd,
arg.Turbo,
arg.EtchingBlock,
arg.EtchingTxHash,
arg.EtchedAt,
)
return err
}
const createRuneEntryState = `-- name: CreateRuneEntryState :exec
INSERT INTO runes_entry_states (rune_id, block_height, mints, burned_amount, completed_at, completed_at_height) VALUES ($1, $2, $3, $4, $5, $6)
`
type CreateRuneEntryStateParams struct {
RuneID string
BlockHeight int32
Mints pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) CreateRuneEntryState(ctx context.Context, arg CreateRuneEntryStateParams) error {
_, err := q.db.Exec(ctx, createRuneEntryState,
arg.RuneID,
arg.BlockHeight,
arg.Mints,
arg.BurnedAmount,
arg.CompletedAt,
arg.CompletedAtHeight,
)
return err
}
const createRuneTransaction = `-- name: CreateRuneTransaction :exec
INSERT INTO runes_transactions (hash, block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type CreateRuneTransactionParams struct {
Hash string
BlockHeight int32
Index int32
Timestamp pgtype.Timestamp
Inputs []byte
Outputs []byte
Mints []byte
Burns []byte
RuneEtched bool
}
func (q *Queries) CreateRuneTransaction(ctx context.Context, arg CreateRuneTransactionParams) error {
_, err := q.db.Exec(ctx, createRuneTransaction,
arg.Hash,
arg.BlockHeight,
arg.Index,
arg.Timestamp,
arg.Inputs,
arg.Outputs,
arg.Mints,
arg.Burns,
arg.RuneEtched,
)
return err
}
const createRunestone = `-- name: CreateRunestone :exec
INSERT INTO runes_runestones (tx_hash, block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
`
type CreateRunestoneParams struct {
TxHash string
BlockHeight int32
Etching bool
EtchingDivisibility pgtype.Int2
EtchingPremine pgtype.Numeric
EtchingRune pgtype.Text
EtchingSpacers pgtype.Int4
EtchingSymbol pgtype.Int4
EtchingTerms pgtype.Bool
EtchingTermsAmount pgtype.Numeric
EtchingTermsCap pgtype.Numeric
EtchingTermsHeightStart pgtype.Int4
EtchingTermsHeightEnd pgtype.Int4
EtchingTermsOffsetStart pgtype.Int4
EtchingTermsOffsetEnd pgtype.Int4
EtchingTurbo pgtype.Bool
Edicts []byte
Mint pgtype.Text
Pointer pgtype.Int4
Cenotaph bool
Flaws int32
}
func (q *Queries) CreateRunestone(ctx context.Context, arg CreateRunestoneParams) error {
_, err := q.db.Exec(ctx, createRunestone,
arg.TxHash,
arg.BlockHeight,
arg.Etching,
arg.EtchingDivisibility,
arg.EtchingPremine,
arg.EtchingRune,
arg.EtchingSpacers,
arg.EtchingSymbol,
arg.EtchingTerms,
arg.EtchingTermsAmount,
arg.EtchingTermsCap,
arg.EtchingTermsHeightStart,
arg.EtchingTermsHeightEnd,
arg.EtchingTermsOffsetStart,
arg.EtchingTermsOffsetEnd,
arg.EtchingTurbo,
arg.Edicts,
arg.Mint,
arg.Pointer,
arg.Cenotaph,
arg.Flaws,
)
return err
}
const deleteIndexedBlockSinceHeight = `-- name: DeleteIndexedBlockSinceHeight :exec
DELETE FROM runes_indexed_blocks WHERE height >= $1
`
@@ -428,6 +266,120 @@ 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
COALESCE(runes_entries.terms_amount, 0) != 0 AND
COALESCE(runes_entries.terms_cap, 0) != 0 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 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 +417,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,19 +1022,41 @@ func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetR
return items, nil
}
const spendOutPointBalances = `-- name: SpendOutPointBalances :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
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 SpendOutPointBalancesParams struct {
SpentHeight pgtype.Int4
TxHash string
TxIdx int32
type GetTotalHoldersByRuneIdsParams struct {
RuneIds []string
BlockHeight int32
}
func (q *Queries) SpendOutPointBalances(ctx context.Context, arg SpendOutPointBalancesParams) error {
_, err := q.db.Exec(ctx, spendOutPointBalances, arg.SpentHeight, arg.TxHash, arg.TxIdx)
return err
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 unspendOutPointBalancesSinceHeight = `-- name: UnspendOutPointBalancesSinceHeight :exec

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ func mapIndexerStateTypeToParams(src entity.IndexerState) gen.SetIndexerStatePar
}
}
func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntry, error) {
func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {
return runes.RuneEntry{}, errors.Wrap(err, "failed to parse rune id")
@@ -153,21 +153,21 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntr
}, nil
}
func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.CreateRuneEntryParams, gen.CreateRuneEntryStateParams, error) {
func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.CreateRuneEntriesParams, gen.CreateRuneEntryStatesParams, error) {
runeId := src.RuneId.String()
rune := src.SpacedRune.Rune.String()
spacers := int32(src.SpacedRune.Spacers)
mints, err := numericFromUint128(&src.Mints)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse mints")
return gen.CreateRuneEntriesParams{}, gen.CreateRuneEntryStatesParams{}, errors.Wrap(err, "failed to parse mints")
}
burnedAmount, err := numericFromUint128(&src.BurnedAmount)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse burned amount")
return gen.CreateRuneEntriesParams{}, gen.CreateRuneEntryStatesParams{}, errors.Wrap(err, "failed to parse burned amount")
}
premine, err := numericFromUint128(&src.Premine)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse premine")
return gen.CreateRuneEntriesParams{}, gen.CreateRuneEntryStatesParams{}, errors.Wrap(err, "failed to parse premine")
}
var completedAt pgtype.Timestamp
if !src.CompletedAt.IsZero() {
@@ -187,13 +187,13 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.Crea
if src.Terms.Amount != nil {
termsAmount, err = numericFromUint128(src.Terms.Amount)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse terms amount")
return gen.CreateRuneEntriesParams{}, gen.CreateRuneEntryStatesParams{}, errors.Wrap(err, "failed to parse terms amount")
}
}
if src.Terms.Cap != nil {
termsCap, err = numericFromUint128(src.Terms.Cap)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse terms cap")
return gen.CreateRuneEntriesParams{}, gen.CreateRuneEntryStatesParams{}, errors.Wrap(err, "failed to parse terms cap")
}
}
if src.Terms.HeightStart != nil {
@@ -221,9 +221,9 @@ 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{
return gen.CreateRuneEntriesParams{
RuneID: runeId,
Rune: rune,
Number: int64(src.Number),
@@ -242,7 +242,7 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.Crea
EtchingBlock: int32(src.EtchingBlock),
EtchingTxHash: src.EtchingTxHash.String(),
EtchedAt: etchedAt,
}, gen.CreateRuneEntryStateParams{
}, gen.CreateRuneEntryStatesParams{
BlockHeight: int32(blockHeight),
RuneID: runeId,
Mints: mints,
@@ -253,7 +253,7 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.Crea
}
// mapRuneTransactionModelToType returns params for creating a new rune transaction and (optionally) a runestone.
func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneTransactionParams, *gen.CreateRunestoneParams, error) {
func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneTransactionsParams, *gen.CreateRunestonesParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp.Time = src.Timestamp
@@ -261,11 +261,11 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}
inputsBytes, err := json.Marshal(src.Inputs)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal inputs")
return gen.CreateRuneTransactionsParams{}, nil, errors.Wrap(err, "failed to marshal inputs")
}
outputsBytes, err := json.Marshal(src.Outputs)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal outputs")
return gen.CreateRuneTransactionsParams{}, nil, errors.Wrap(err, "failed to marshal outputs")
}
mints := make(map[string]uint128.Uint128)
for key, value := range src.Mints {
@@ -273,7 +273,7 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}
mintsBytes, err := json.Marshal(mints)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal mints")
return gen.CreateRuneTransactionsParams{}, nil, errors.Wrap(err, "failed to marshal mints")
}
burns := make(map[string]uint128.Uint128)
for key, value := range src.Burns {
@@ -281,19 +281,19 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}
burnsBytes, err := json.Marshal(burns)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal burns")
return gen.CreateRuneTransactionsParams{}, nil, errors.Wrap(err, "failed to marshal burns")
}
var runestoneParams *gen.CreateRunestoneParams
var runestoneParams *gen.CreateRunestonesParams
if src.Runestone != nil {
params, err := mapRunestoneTypeToParams(*src.Runestone, src.Hash, src.BlockHeight)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to map runestone to params")
return gen.CreateRuneTransactionsParams{}, nil, errors.Wrap(err, "failed to map runestone to params")
}
runestoneParams = &params
}
return gen.CreateRuneTransactionParams{
return gen.CreateRuneTransactionsParams{
Hash: src.Hash.String(),
BlockHeight: int32(src.BlockHeight),
Index: int32(src.Index),
@@ -409,15 +409,15 @@ func mapRuneTransactionModelToType(src gen.RunesTransaction) (entity.RuneTransac
}, nil
}
func mapRunestoneTypeToParams(src runes.Runestone, txHash chainhash.Hash, blockHeight uint64) (gen.CreateRunestoneParams, error) {
var runestoneParams gen.CreateRunestoneParams
func mapRunestoneTypeToParams(src runes.Runestone, txHash chainhash.Hash, blockHeight uint64) (gen.CreateRunestonesParams, error) {
var runestoneParams gen.CreateRunestonesParams
// TODO: optimize serialized edicts
edictsBytes, err := json.Marshal(src.Edicts)
if err != nil {
return gen.CreateRunestoneParams{}, errors.Wrap(err, "failed to marshal runestone edicts")
return gen.CreateRunestonesParams{}, errors.Wrap(err, "failed to marshal runestone edicts")
}
runestoneParams = gen.CreateRunestoneParams{
runestoneParams = gen.CreateRunestonesParams{
TxHash: txHash.String(),
BlockHeight: int32(blockHeight),
Edicts: edictsBytes,
@@ -433,7 +433,7 @@ func mapRunestoneTypeToParams(src runes.Runestone, txHash chainhash.Hash, blockH
if etching.Premine != nil {
premine, err := numericFromUint128(etching.Premine)
if err != nil {
return gen.CreateRunestoneParams{}, errors.Wrap(err, "failed to parse etching premine")
return gen.CreateRunestonesParams{}, errors.Wrap(err, "failed to parse etching premine")
}
runestoneParams.EtchingPremine = premine
}
@@ -452,14 +452,14 @@ func mapRunestoneTypeToParams(src runes.Runestone, txHash chainhash.Hash, blockH
if terms.Amount != nil {
amount, err := numericFromUint128(terms.Amount)
if err != nil {
return gen.CreateRunestoneParams{}, errors.Wrap(err, "failed to parse etching terms amount")
return gen.CreateRunestonesParams{}, errors.Wrap(err, "failed to parse etching terms amount")
}
runestoneParams.EtchingTermsAmount = amount
}
if terms.Cap != nil {
cap, err := numericFromUint128(terms.Cap)
if err != nil {
return gen.CreateRunestoneParams{}, errors.Wrap(err, "failed to parse etching terms cap")
return gen.CreateRunestonesParams{}, errors.Wrap(err, "failed to parse etching terms cap")
}
runestoneParams.EtchingTermsCap = cap
}

View File

@@ -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,49 +459,113 @@ func (r *Repository) GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript
return result, nil
}
func (r *Repository) CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error {
if tx == 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) CreateRuneTransactions(ctx context.Context, txs []*entity.RuneTransaction) error {
if len(txs) == 0 {
return nil
}
txParams, runestoneParams, err := mapRuneTransactionTypeToParams(*tx)
if err != nil {
return errors.Wrap(err, "failed to map rune transaction to params")
}
if err = r.queries.CreateRuneTransaction(ctx, txParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneTransaction")
}
if runestoneParams != nil {
if err = r.queries.CreateRunestone(ctx, *runestoneParams); err != nil {
return errors.Wrap(err, "error during exec CreateRunestone")
txParams := make([]gen.CreateRuneTransactionsParams, 0, len(txs))
runestoneParams := make([]gen.CreateRunestonesParams, 0, len(txs))
for _, tx := range txs {
txParam, runestoneParam, err := mapRuneTransactionTypeToParams(*tx)
if err != nil {
return errors.Wrap(err, "failed to map rune transaction to params")
}
txParams = append(txParams, txParam)
if runestoneParam != nil {
runestoneParams = append(runestoneParams, *runestoneParam)
}
}
return nil
}
createTxResults := r.queries.CreateRuneTransactions(ctx, txParams)
var execErrors []error
createTxResults.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec CreateRuneTransactions")
}
func (r *Repository) CreateRuneEntry(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
return nil
}
createParams, _, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
}
if err = r.queries.CreateRuneEntry(ctx, createParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntry")
createRunestoneResults := r.queries.CreateRunestones(ctx, runestoneParams)
execErrors = make([]error, 0)
createRunestoneResults.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec CreateRunestones")
}
return nil
}
func (r *Repository) CreateRuneEntryState(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
func (r *Repository) CreateRuneEntries(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error {
if len(entries) == 0 {
return nil
}
_, createStateParams, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
createParams := make([]gen.CreateRuneEntriesParams, 0, len(entries))
for _, entry := range entries {
param, _, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
}
createParams = append(createParams, param)
}
if err = r.queries.CreateRuneEntryState(ctx, createStateParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntryState")
results := r.queries.CreateRuneEntries(ctx, createParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec CreateRuneEntries")
}
return nil
}
func (r *Repository) CreateRuneEntryStates(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error {
if len(entries) == 0 {
return nil
}
createParams := make([]gen.CreateRuneEntryStatesParams, 0, len(entries))
for _, entry := range entries {
_, param, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
}
createParams = append(createParams, param)
}
results := r.queries.CreateRuneEntryStates(ctx, createParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec CreateRuneEntryStates")
}
return nil
}
@@ -469,13 +592,25 @@ func (r *Repository) CreateOutPointBalances(ctx context.Context, outPointBalance
return nil
}
func (r *Repository) SpendOutPointBalances(ctx context.Context, outPoint wire.OutPoint, blockHeight uint64) error {
if err := r.queries.SpendOutPointBalances(ctx, gen.SpendOutPointBalancesParams{
TxHash: outPoint.Hash.String(),
TxIdx: int32(outPoint.Index),
SpentHeight: pgtype.Int4{Int32: int32(blockHeight), Valid: true},
}); err != nil {
return errors.Wrap(err, "error during exec")
func (r *Repository) SpendOutPointBalancesBatch(ctx context.Context, outPoints []wire.OutPoint, blockHeight uint64) error {
params := make([]gen.SpendOutPointBalancesBatchParams, 0, len(outPoints))
for _, outPoint := range outPoints {
params = append(params, gen.SpendOutPointBalancesBatchParams{
TxHash: outPoint.Hash.String(),
TxIdx: int32(outPoint.Index),
SpentHeight: pgtype.Int4{Int32: int32(blockHeight), Valid: true},
})
}
results := r.queries.SpendOutPointBalancesBatch(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}

View File

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

View File

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

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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]))
})
}

View File

@@ -14,6 +14,12 @@ import (
"github.com/valyala/fasthttp"
)
var DefaultClient = fasthttp.Client{
MaxConnsPerHost: 10240, // default is 512
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
}
type Config struct {
// Enable debug mode
Debug bool
@@ -143,7 +149,7 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
fasthttp.ReleaseRequest(req)
}()
if err := fasthttp.Do(req, resp); err != nil {
if err := DefaultClient.Do(req, resp); err != nil {
return nil, errors.Wrapf(err, "url: %s", url)
}