Compare commits

...

31 Commits
v0.4.5 ... main

Author SHA1 Message Date
Gaze
58f8497997 feat: add code to errs.PublicError 2024-11-26 14:39:27 +07:00
Gaze
920f7fe07b chore: go mod tidy 2024-11-22 14:23:53 +07:00
Gaze
0cb66232ef feat: add bip322 pkg 2024-11-22 14:22:07 +07:00
gazenw
4074548b3e Merge pull request #74 from gaze-network/develop
Release 0.7.0
2024-10-31 14:18:59 +07:00
gazenw
c5c9a7bdeb feat: add get Runes info batch api (#73)
* fix: make existing handlers use new total holders usecase

* fix: error msg

* feat: add get token info batch

* feat: add includeHoldersCount in get tokens api

* refactor: extract response mapping

* fix: rename new field and add holdersCount to extend

* fix: query params array

* fix: error msg

* fix: struct tags

* fix: remove error

* feat: add default value to additional fields
2024-10-31 14:14:58 +07:00
gazenw
58334dd3e4 Merge pull request #72 from gaze-network/develop
feat: add etching tx hash for runes info api
2024-10-26 20:56:01 +07:00
Gaze
cffe378beb feat: add etching tx hash for runes info api 2024-10-25 16:22:24 +07:00
gazenw
9a7ee49228 Merge pull request #71 from gaze-network/develop
Release v0.6.0
2024-10-17 14:35:19 +07:00
gazenw
9739f61067 feat: implement batch insert using multirow inserts (#70)
* feat: add new batch inserts

* fix: migration

* fix: add casting to unnest with patch

* fix: add UTC() to timestamp mappers

* chore: unused imports

* chore: remove unnecessary comments
2024-10-17 14:34:04 +07:00
gazenw
f1267b387e Merge pull request #69 from gaze-network/develop
Release v0.5.6
2024-10-15 17:59:34 +07:00
gazenw
8883c24c77 fix: only call VerifyStates if not api only (#68) 2024-10-15 17:58:51 +07:00
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
1dc57d74e0 Merge remote-tracking branch 'origin/main' into develop 2024-10-05 01:38:34 +07:00
46 changed files with 2623 additions and 505 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

@@ -11,6 +11,7 @@ import (
type PublicError struct {
err error
message string
code string // code is optional, it can be used to identify the error type
}
func (p PublicError) Error() string {
@@ -21,6 +22,10 @@ func (p PublicError) Message() string {
return p.message
}
func (p PublicError) Code() string {
return p.code
}
func (p PublicError) Unwrap() error {
return p.err
}
@@ -29,6 +34,10 @@ func NewPublicError(message string) error {
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message}, 1)
}
func NewPublicErrorWithCode(message string, code string) error {
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message, code: code}, 1)
}
func WithPublicMessage(err error, prefix string) error {
if err == nil {
return nil
@@ -41,3 +50,16 @@ func WithPublicMessage(err error, prefix string) error {
}
return withstack.WithStackDepth(&PublicError{err: err, message: message}, 1)
}
func WithPublicMessageCode(err error, prefix string, code string) error {
if err == nil {
return nil
}
var message string
if prefix != "" {
message = fmt.Sprintf("%s: %s", prefix, err.Error())
} else {
message = err.Error()
}
return withstack.WithStackDepth(&PublicError{err: err, message: message, code: code}, 1)
}

2
go.sum
View File

@@ -99,6 +99,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -232,6 +233,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=

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,8 @@ package httphandler
import (
"bytes"
"encoding/hex"
"fmt"
"net/url"
"slices"
"github.com/cockroachdb/errors"
@@ -15,21 +17,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 +73,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,16 +86,12 @@ 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
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
}
}

View File

@@ -1,11 +1,12 @@
package httphandler
import (
"slices"
"fmt"
"net/url"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
"github.com/gofiber/fiber/v2"
@@ -13,15 +14,29 @@ import (
)
type getTokenInfoRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
AdditionalFields []string
}
func (r getTokenInfoRequest) Validate() error {
func (r *getTokenInfoRequest) Validate() error {
var errList []error
if !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
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.AdditionalFieldsRaw == "" {
// temporarily set default value for backward compatibility
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
}
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -35,17 +50,19 @@ type entryTerms struct {
}
type entry struct {
Divisibility uint8 `json:"divisibility"`
Premine uint128.Uint128 `json:"premine"`
Rune runes.Rune `json:"rune"`
Spacers uint32 `json:"spacers"`
Symbol string `json:"symbol"`
Terms entryTerms `json:"terms"`
Turbo bool `json:"turbo"`
Divisibility uint8 `json:"divisibility"`
Premine uint128.Uint128 `json:"premine"`
Rune runes.Rune `json:"rune"`
Spacers uint32 `json:"spacers"`
Symbol string `json:"symbol"`
Terms entryTerms `json:"terms"`
Turbo bool `json:"turbo"`
EtchingTxHash string `json:"etchingTxHash"`
}
type tokenInfoExtend struct {
Entry entry `json:"entry"`
HoldersCount *int64 `json:"holdersCount,omitempty"`
Entry entry `json:"entry"`
}
type getTokenInfoResult struct {
@@ -57,11 +74,11 @@ 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"`
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
Extend tokenInfoExtend `json:"extend"`
}
@@ -96,7 +113,7 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
var ok bool
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
}
}
@@ -105,70 +122,78 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetTokenInfoByHeight")
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
}
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
var holdersCountPtr *int64
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCount, err := h.usecase.GetTotalHoldersByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
holdersCountPtr = &holdersCount
}
holdingBalances = lo.Filter(holdingBalances, func(b *entity.Balance, _ int) bool {
return !b.Amount.IsZero()
})
// sort by amount descending
slices.SortFunc(holdingBalances, func(i, j *entity.Balance) int {
return j.Amount.Cmp(i.Amount)
})
totalSupply, err := runeEntry.Supply()
result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
if err != nil {
return errors.Wrap(err, "cannot get total supply of rune")
return errors.Wrap(err, "error during createTokenInfoResult")
}
mintedAmount, err := runeEntry.MintedAmount()
if err != nil {
return errors.Wrap(err, "cannot get minted amount of rune")
}
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
terms := lo.FromPtr(runeEntry.Terms)
resp := getTokenInfoResponse{
Result: &getTokenInfoResult{
Id: runeId,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
TotalSupply: totalSupply,
CirculatingSupply: circulatingSupply,
MintedAmount: mintedAmount,
BurnedAmount: runeEntry.BurnedAmount,
Decimals: runeEntry.Divisibility,
DeployedAt: uint64(runeEntry.EtchedAt.Unix()),
DeployedAtHeight: runeEntry.EtchingBlock,
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(uint64(runeEntry.CompletedAt.Unix()))),
CompletedAtHeight: runeEntry.CompletedAtHeight,
HoldersCount: len(holdingBalances),
Extend: tokenInfoExtend{
Entry: entry{
Divisibility: runeEntry.Divisibility,
Premine: runeEntry.Premine,
Rune: runeEntry.SpacedRune.Rune,
Spacers: runeEntry.SpacedRune.Spacers,
Symbol: string(runeEntry.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: runeEntry.Turbo,
},
},
},
Result: result,
}
return errors.WithStack(ctx.JSON(resp))
}
func createTokenInfoResult(runeEntry *runes.RuneEntry, holdersCount *int64) (*getTokenInfoResult, error) {
totalSupply, err := runeEntry.Supply()
if err != nil {
return nil, errors.Wrap(err, "cannot get total supply of rune")
}
mintedAmount, err := runeEntry.MintedAmount()
if err != nil {
return nil, errors.Wrap(err, "cannot get minted amount of rune")
}
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
terms := lo.FromPtr(runeEntry.Terms)
return &getTokenInfoResult{
Id: runeEntry.RuneId,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
TotalSupply: totalSupply,
CirculatingSupply: circulatingSupply,
MintedAmount: mintedAmount,
BurnedAmount: runeEntry.BurnedAmount,
Decimals: runeEntry.Divisibility,
DeployedAt: runeEntry.EtchedAt.Unix(),
DeployedAtHeight: runeEntry.EtchingBlock,
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
CompletedAtHeight: runeEntry.CompletedAtHeight,
HoldersCount: lo.FromPtr(holdersCount),
Extend: tokenInfoExtend{
HoldersCount: holdersCount,
Entry: entry{
Divisibility: runeEntry.Divisibility,
Premine: runeEntry.Premine,
Rune: runeEntry.SpacedRune.Rune,
Spacers: runeEntry.SpacedRune.Spacers,
Symbol: string(runeEntry.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: runeEntry.Turbo,
EtchingTxHash: runeEntry.EtchingTxHash.String(),
},
},
}, nil
}

View File

@@ -0,0 +1,118 @@
package httphandler
import (
"fmt"
"net/url"
"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 getTokenInfoBatchRequest struct {
Ids []string `json:"ids"`
BlockHeight uint64 `json:"blockHeight"`
AdditionalFields []string `json:"additionalFields"`
}
const getTokenInfoBatchMaxQueries = 100
func (r *getTokenInfoBatchRequest) Validate() error {
var errList []error
if len(r.Ids) == 0 {
errList = append(errList, errors.New("ids cannot be empty"))
}
if len(r.Ids) > getTokenInfoBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot query more than %d ids", getTokenInfoBatchMaxQueries))
}
for i := range r.Ids {
id, err := url.QueryUnescape(r.Ids[i])
if err != nil {
return errors.WithStack(err)
}
r.Ids[i] = id
if !isRuneIdOrRuneName(r.Ids[i]) {
errList = append(errList, errors.Errorf("ids[%d]: id '%s' is not valid rune id or rune name", i, r.Ids[i]))
}
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getTokenInfoBatchResult struct {
List []*getTokenInfoResult `json:"list"`
}
type getTokenInfoBatchResponse = HttpResponse[getTokenInfoBatchResult]
func (h *HttpHandler) GetTokenInfoBatch(ctx *fiber.Ctx) (err error) {
var req getTokenInfoBatchRequest
if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
runeIds := make([]runes.RuneId, 0)
for i, id := range req.Ids {
runeId, ok := h.resolveRuneId(ctx.UserContext(), id)
if !ok {
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"ids[%d]\"", id, i))
}
runeIds = append(runeIds, runeId)
}
runeEntries, err := h.usecase.GetRuneEntryByRuneIdAndHeightBatch(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeightBatch")
}
holdersCounts := make(map[runes.RuneId]int64)
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
}
results := make([]*getTokenInfoResult, 0, len(runeIds))
for _, runeId := range runeIds {
runeEntry, ok := runeEntries[runeId]
if !ok {
return errs.NewPublicError(fmt.Sprintf("rune not found: %s", runeId))
}
var holdersCount *int64
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCount = lo.ToPtr(holdersCounts[runeId])
}
result, err := createTokenInfoResult(runeEntry, holdersCount)
if err != nil {
return errors.Wrap(err, "error during createTokenInfoResult")
}
results = append(results, result)
}
resp := getTokenInfoBatchResponse{
Result: &getTokenInfoBatchResult{
List: results,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -0,0 +1,150 @@
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"`
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
AdditionalFields []string
}
func (r *getTokensRequest) Validate() error {
var errList []error
if err := r.paginationRequest.Validate(); err != nil {
errList = append(errList, err)
}
if r.Limit > getTokensMaxLimit {
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
}
if r.Scope != "" && !r.Scope.IsValid() {
errList = append(errList, errors.Errorf("invalid scope: %s", r.Scope))
}
if r.AdditionalFieldsRaw == "" {
// temporarily set default value for backward compatibility
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
}
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
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 })
holdersCounts := make(map[runes.RuneId]int64)
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
}
}
results := make([]*getTokenInfoResult, 0, len(entries))
for _, ent := range entries {
var holdersCount *int64
if lo.Contains(req.AdditionalFields, "holdersCount") {
holdersCount = lo.ToPtr(holdersCounts[ent.RuneId])
}
result, err := createTokenInfoResult(ent, holdersCount)
if err != nil {
return errors.Wrap(err, "error during createTokenInfoResult")
}
results = append(results, result)
}
return errors.WithStack(ctx.JSON(getTokensResponse{
Result: &getTokensResult{
List: results,
},
}))
}

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 != "" {
@@ -143,12 +152,9 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
var ok bool
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.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,14 @@ 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.Post("/info/batch", h.GetTokenInfoBatch)
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

@@ -67,7 +67,7 @@ var GenesisRuneConfigMap = map[common.Network]GenesisRuneConfig{
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Time{},
EtchedAt: time.Unix(0, 0),
},
common.NetworkFractalMainnet: {
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
@@ -86,7 +86,7 @@ var GenesisRuneConfigMap = map[common.Network]GenesisRuneConfig{
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Time{},
EtchedAt: time.Unix(0, 0),
},
common.NetworkFractalTestnet: {
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
@@ -105,7 +105,7 @@ var GenesisRuneConfigMap = map[common.Network]GenesisRuneConfig{
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Time{},
EtchedAt: time.Unix(0, 0),
},
}

View File

@@ -1,5 +1,6 @@
BEGIN;
CREATE EXTENSION IF NOT EXISTS 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

@@ -0,0 +1,104 @@
-- name: BatchCreateRunesBalances :exec
INSERT INTO runes_balances ("pkscript", "block_height", "rune_id", "amount")
VALUES(
unnest(@pkscript_arr::TEXT[]),
unnest(@block_height_arr::INT[]),
unnest(@rune_id_arr::TEXT[]),
unnest(@amount_arr::DECIMAL[])
);
-- name: BatchCreateRuneEntries :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(
unnest(@rune_id_arr::TEXT[]),
unnest(@rune_arr::TEXT[]),
unnest(@number_arr::BIGINT[]),
unnest(@spacers_arr::INT[]),
unnest(@premine_arr::DECIMAL[]),
unnest(@symbol_arr::INT[]),
unnest(@divisibility_arr::SMALLINT[]),
unnest(@terms_arr::BOOLEAN[]),
unnest(@terms_amount_arr::DECIMAL[]),
unnest(@terms_cap_arr::DECIMAL[]),
unnest(@terms_height_start_arr::INT[]), -- nullable (need patch)
unnest(@terms_height_end_arr::INT[]), -- nullable (need patch)
unnest(@terms_offset_start_arr::INT[]), -- nullable (need patch)
unnest(@terms_offset_end_arr::INT[]), -- nullable (need patch)
unnest(@turbo_arr::BOOLEAN[]),
unnest(@etching_block_arr::INT[]),
unnest(@etching_tx_hash_arr::TEXT[]),
unnest(@etched_at_arr::TIMESTAMP[])
);
-- name: BatchCreateRuneEntryStates :exec
INSERT INTO runes_entry_states ("rune_id", "block_height", "mints", "burned_amount", "completed_at", "completed_at_height")
VALUES(
unnest(@rune_id_arr::TEXT[]),
unnest(@block_height_arr::INT[]),
unnest(@mints_arr::DECIMAL[]),
unnest(@burned_amount_arr::DECIMAL[]),
unnest(@completed_at_arr::TIMESTAMP[]),
unnest(@completed_at_height_arr::INT[]) -- nullable (need patch)
);
-- name: BatchCreateRunesOutpointBalances :exec
INSERT INTO runes_outpoint_balances ("rune_id", "pkscript", "tx_hash", "tx_idx", "amount", "block_height", "spent_height")
VALUES(
unnest(@rune_id_arr::TEXT[]),
unnest(@pkscript_arr::TEXT[]),
unnest(@tx_hash_arr::TEXT[]),
unnest(@tx_idx_arr::INT[]),
unnest(@amount_arr::DECIMAL[]),
unnest(@block_height_arr::INT[]),
unnest(@spent_height_arr::INT[]) -- nullable (need patch)
);
-- name: BatchSpendOutpointBalances :exec
UPDATE runes_outpoint_balances
SET "spent_height" = @spent_height::INT
FROM (
SELECT
unnest(@tx_hash_arr::TEXT[]) AS tx_hash,
unnest(@tx_idx_arr::INT[]) AS tx_idx
) AS input
WHERE "runes_outpoint_balances"."tx_hash" = "input"."tx_hash" AND "runes_outpoint_balances"."tx_idx" = "input"."tx_idx";
-- name: BatchCreateRunestones :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(
unnest(@tx_hash_arr::TEXT[]),
unnest(@block_height_arr::INT[]),
unnest(@etching_arr::BOOLEAN[]),
unnest(@etching_divisibility_arr::SMALLINT[]), -- nullable (need patch)
unnest(@etching_premine_arr::DECIMAL[]),
unnest(@etching_rune_arr::TEXT[]), -- nullable (need patch)
unnest(@etching_spacers_arr::INT[]), -- nullable (need patch)
unnest(@etching_symbol_arr::INT[]), -- nullable (need patch)
unnest(@etching_terms_arr::BOOLEAN[]), -- nullable (need patch)
unnest(@etching_terms_amount_arr::DECIMAL[]),
unnest(@etching_terms_cap_arr::DECIMAL[]),
unnest(@etching_terms_height_start_arr::INT[]), -- nullable (need patch)
unnest(@etching_terms_height_end_arr::INT[]), -- nullable (need patch)
unnest(@etching_terms_offset_start_arr::INT[]), -- nullable (need patch)
unnest(@etching_terms_offset_end_arr::INT[]), -- nullable (need patch)
unnest(@etching_turbo_arr::BOOLEAN[]), -- nullable (need patch)
unnest(@edicts_arr::JSONB[]),
unnest(@mint_arr::TEXT[]), -- nullable (need patch)
unnest(@pointer_arr::INT[]), -- nullable (need patch)
unnest(@cenotaph_arr::BOOLEAN[]),
unnest(@flaws_arr::INT[])
);
-- name: BatchCreateRuneTransactions :exec
INSERT INTO runes_transactions ("hash", "block_height", "index", "timestamp", "inputs", "outputs", "mints", "burns", "rune_etched")
VALUES (
unnest(@hash_arr::TEXT[]),
unnest(@block_height_arr::INT[]),
unnest(@index_arr::INT[]),
unnest(@timestamp_arr::TIMESTAMP[]),
unnest(@inputs_arr::JSONB[]),
unnest(@outputs_arr::JSONB[]),
unnest(@mints_arr::JSONB[]),
unnest(@burns_arr::JSONB[]),
unnest(@rune_etched_arr::BOOLEAN[])
);

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;
@@ -101,13 +150,13 @@ INSERT INTO runes_transactions (hash, block_height, index, timestamp, inputs, ou
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
-- name: CreateOutPointBalance :exec
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: SpendOutPointBalance :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3;
-- name: CreateRuneBalanceAtBlock :batchexec
-- name: CreateRuneBalance :exec
INSERT INTO runes_balances (pkscript, block_height, rune_id, amount) VALUES ($1, $2, $3, $4);
-- name: GetLatestIndexedBlock :one

View File

@@ -8,7 +8,6 @@ import (
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
)
type RunesDataGateway interface {
@@ -44,6 +43,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 +59,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) 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
CreateRuneBalances(ctx context.Context, params []CreateRuneBalancesParams) error
CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error
SpendOutPointBalancesBatch(ctx context.Context, outPoints []wire.OutPoint, blockHeight uint64) error
CreateRuneBalances(ctx context.Context, params []*entity.Balance) 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)?
@@ -77,10 +82,3 @@ type RunesWriterDataGateway interface {
UnspendOutPointBalancesSinceHeight(ctx context.Context, height uint64) error
DeleteRuneBalancesSinceHeight(ctx context.Context, height uint64) error
}
type CreateRuneBalancesParams struct {
PkScript []byte
RuneId runes.RuneId
Balance uint128.Uint128
BlockHeight uint64
}

View File

@@ -145,7 +145,10 @@ func (p *Processor) ensureGenesisRune(ctx context.Context, network common.Networ
EtchingTxHash: genesisRuneConfig.EtchingTxHash,
EtchedAt: genesisRuneConfig.EtchedAt,
}
if err := p.runesDg.CreateRuneEntry(ctx, runeEntry, genesisRuneConfig.RuneId.BlockHeight); err != nil {
if err := p.runesDg.CreateRuneEntries(ctx, []*runes.RuneEntry{runeEntry}); 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

@@ -14,7 +14,6 @@ import (
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/runes/constants"
"github.com/gaze-network/indexer-network/modules/runes/datagateway"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/indexer-network/pkg/logger"
@@ -27,19 +26,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 +675,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")
@@ -715,78 +722,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); 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)
newOutpointBalances := make([]*entity.OutPointBalance, 0)
for _, balances := range p.newOutPointBalances {
newOutpointBalances = append(newOutpointBalances, balances...)
}
if err := runesDgTx.CreateOutPointBalances(ctx, newOutpointBalances); 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")
}
// 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),
})
}
p.newSpendOutPoints = make([]wire.OutPoint, 0)
// flush new newBalances
newBalances := make([]*entity.Balance, 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 {
newBalances = append(newBalances, &entity.Balance{
PkScript: pkScript,
RuneId: runeId,
Amount: balance,
BlockHeight: uint64(blockHeader.Height),
})
}
p.newBalances = make(map[string]map[runes.RuneId]uint128.Uint128)
}
if err := runesDgTx.CreateRuneBalances(ctx, newBalances); 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(newOutpointBalances)),
slog.Int("new_spend_outpoints", len(newSpendOutPoints)),
slog.Int("new_balances", len(newBalances)),
slog.Int("new_rune_txs", len(newRuneTxs)),
slogx.Duration("time_taken", timeTaken),
)
// submit event to reporting system
if p.reportingClient != nil {

View File

@@ -1,130 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: batch.go
package gen
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
var (
ErrBatchAlreadyClosed = errors.New("batch already closed")
)
const createOutPointBalances = `-- 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)
`
type CreateOutPointBalancesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateOutPointBalancesParams struct {
RuneID string
Pkscript string
TxHash string
TxIdx int32
Amount pgtype.Numeric
BlockHeight int32
SpentHeight pgtype.Int4
}
func (q *Queries) CreateOutPointBalances(ctx context.Context, arg []CreateOutPointBalancesParams) *CreateOutPointBalancesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.RuneID,
a.Pkscript,
a.TxHash,
a.TxIdx,
a.Amount,
a.BlockHeight,
a.SpentHeight,
}
batch.Queue(createOutPointBalances, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateOutPointBalancesBatchResults{br, len(arg), false}
}
func (b *CreateOutPointBalancesBatchResults) 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 *CreateOutPointBalancesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createRuneBalanceAtBlock = `-- name: CreateRuneBalanceAtBlock :batchexec
INSERT INTO runes_balances (pkscript, block_height, rune_id, amount) VALUES ($1, $2, $3, $4)
`
type CreateRuneBalanceAtBlockBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateRuneBalanceAtBlockParams struct {
Pkscript string
BlockHeight int32
RuneID string
Amount pgtype.Numeric
}
func (q *Queries) CreateRuneBalanceAtBlock(ctx context.Context, arg []CreateRuneBalanceAtBlockParams) *CreateRuneBalanceAtBlockBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Pkscript,
a.BlockHeight,
a.RuneID,
a.Amount,
}
batch.Queue(createRuneBalanceAtBlock, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateRuneBalanceAtBlockBatchResults{br, len(arg), false}
}
func (b *CreateRuneBalanceAtBlockBatchResults) 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 *CreateRuneBalanceAtBlockBatchResults) Close() error {
b.closed = true
return b.br.Close()
}

View File

@@ -0,0 +1,319 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: batch.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const batchCreateRuneEntries = `-- name: BatchCreateRuneEntries :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(
unnest($1::TEXT[]),
unnest($2::TEXT[]),
unnest($3::BIGINT[]),
unnest($4::INT[]),
unnest($5::DECIMAL[]),
unnest($6::INT[]),
unnest($7::SMALLINT[]),
unnest($8::BOOLEAN[]),
unnest($9::DECIMAL[]),
unnest($10::DECIMAL[]),
unnest($11::INT[]), -- nullable (need patch)
unnest($12::INT[]), -- nullable (need patch)
unnest($13::INT[]), -- nullable (need patch)
unnest($14::INT[]), -- nullable (need patch)
unnest($15::BOOLEAN[]),
unnest($16::INT[]),
unnest($17::TEXT[]),
unnest($18::TIMESTAMP[])
)
`
type BatchCreateRuneEntriesParams struct {
RuneIDArr []string
RuneArr []string
NumberArr []int64
SpacersArr []int32
PremineArr []pgtype.Numeric
SymbolArr []int32
DivisibilityArr []int16
TermsArr []bool
TermsAmountArr []pgtype.Numeric
TermsCapArr []pgtype.Numeric
TermsHeightStartArr []int32
TermsHeightEndArr []int32
TermsOffsetStartArr []int32
TermsOffsetEndArr []int32
TurboArr []bool
EtchingBlockArr []int32
EtchingTxHashArr []string
EtchedAtArr []pgtype.Timestamp
}
func (q *Queries) BatchCreateRuneEntries(ctx context.Context, arg BatchCreateRuneEntriesParams) error {
_, err := q.db.Exec(ctx, batchCreateRuneEntries,
arg.RuneIDArr,
arg.RuneArr,
arg.NumberArr,
arg.SpacersArr,
arg.PremineArr,
arg.SymbolArr,
arg.DivisibilityArr,
arg.TermsArr,
arg.TermsAmountArr,
arg.TermsCapArr,
arg.TermsHeightStartArr,
arg.TermsHeightEndArr,
arg.TermsOffsetStartArr,
arg.TermsOffsetEndArr,
arg.TurboArr,
arg.EtchingBlockArr,
arg.EtchingTxHashArr,
arg.EtchedAtArr,
)
return err
}
const batchCreateRuneEntryStates = `-- name: BatchCreateRuneEntryStates :exec
INSERT INTO runes_entry_states ("rune_id", "block_height", "mints", "burned_amount", "completed_at", "completed_at_height")
VALUES(
unnest($1::TEXT[]),
unnest($2::INT[]),
unnest($3::DECIMAL[]),
unnest($4::DECIMAL[]),
unnest($5::TIMESTAMP[]),
unnest($6::INT[]) -- nullable (need patch)
)
`
type BatchCreateRuneEntryStatesParams struct {
RuneIDArr []string
BlockHeightArr []int32
MintsArr []pgtype.Numeric
BurnedAmountArr []pgtype.Numeric
CompletedAtArr []pgtype.Timestamp
CompletedAtHeightArr []int32
}
func (q *Queries) BatchCreateRuneEntryStates(ctx context.Context, arg BatchCreateRuneEntryStatesParams) error {
_, err := q.db.Exec(ctx, batchCreateRuneEntryStates,
arg.RuneIDArr,
arg.BlockHeightArr,
arg.MintsArr,
arg.BurnedAmountArr,
arg.CompletedAtArr,
arg.CompletedAtHeightArr,
)
return err
}
const batchCreateRuneTransactions = `-- name: BatchCreateRuneTransactions :exec
INSERT INTO runes_transactions ("hash", "block_height", "index", "timestamp", "inputs", "outputs", "mints", "burns", "rune_etched")
VALUES (
unnest($1::TEXT[]),
unnest($2::INT[]),
unnest($3::INT[]),
unnest($4::TIMESTAMP[]),
unnest($5::JSONB[]),
unnest($6::JSONB[]),
unnest($7::JSONB[]),
unnest($8::JSONB[]),
unnest($9::BOOLEAN[])
)
`
type BatchCreateRuneTransactionsParams struct {
HashArr []string
BlockHeightArr []int32
IndexArr []int32
TimestampArr []pgtype.Timestamp
InputsArr [][]byte
OutputsArr [][]byte
MintsArr [][]byte
BurnsArr [][]byte
RuneEtchedArr []bool
}
func (q *Queries) BatchCreateRuneTransactions(ctx context.Context, arg BatchCreateRuneTransactionsParams) error {
_, err := q.db.Exec(ctx, batchCreateRuneTransactions,
arg.HashArr,
arg.BlockHeightArr,
arg.IndexArr,
arg.TimestampArr,
arg.InputsArr,
arg.OutputsArr,
arg.MintsArr,
arg.BurnsArr,
arg.RuneEtchedArr,
)
return err
}
const batchCreateRunesBalances = `-- name: BatchCreateRunesBalances :exec
INSERT INTO runes_balances ("pkscript", "block_height", "rune_id", "amount")
VALUES(
unnest($1::TEXT[]),
unnest($2::INT[]),
unnest($3::TEXT[]),
unnest($4::DECIMAL[])
)
`
type BatchCreateRunesBalancesParams struct {
PkscriptArr []string
BlockHeightArr []int32
RuneIDArr []string
AmountArr []pgtype.Numeric
}
func (q *Queries) BatchCreateRunesBalances(ctx context.Context, arg BatchCreateRunesBalancesParams) error {
_, err := q.db.Exec(ctx, batchCreateRunesBalances,
arg.PkscriptArr,
arg.BlockHeightArr,
arg.RuneIDArr,
arg.AmountArr,
)
return err
}
const batchCreateRunesOutpointBalances = `-- name: BatchCreateRunesOutpointBalances :exec
INSERT INTO runes_outpoint_balances ("rune_id", "pkscript", "tx_hash", "tx_idx", "amount", "block_height", "spent_height")
VALUES(
unnest($1::TEXT[]),
unnest($2::TEXT[]),
unnest($3::TEXT[]),
unnest($4::INT[]),
unnest($5::DECIMAL[]),
unnest($6::INT[]),
unnest($7::INT[]) -- nullable (need patch)
)
`
type BatchCreateRunesOutpointBalancesParams struct {
RuneIDArr []string
PkscriptArr []string
TxHashArr []string
TxIdxArr []int32
AmountArr []pgtype.Numeric
BlockHeightArr []int32
SpentHeightArr []int32
}
func (q *Queries) BatchCreateRunesOutpointBalances(ctx context.Context, arg BatchCreateRunesOutpointBalancesParams) error {
_, err := q.db.Exec(ctx, batchCreateRunesOutpointBalances,
arg.RuneIDArr,
arg.PkscriptArr,
arg.TxHashArr,
arg.TxIdxArr,
arg.AmountArr,
arg.BlockHeightArr,
arg.SpentHeightArr,
)
return err
}
const batchCreateRunestones = `-- name: BatchCreateRunestones :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(
unnest($1::TEXT[]),
unnest($2::INT[]),
unnest($3::BOOLEAN[]),
unnest($4::SMALLINT[]), -- nullable (need patch)
unnest($5::DECIMAL[]),
unnest($6::TEXT[]), -- nullable (need patch)
unnest($7::INT[]), -- nullable (need patch)
unnest($8::INT[]), -- nullable (need patch)
unnest($9::BOOLEAN[]), -- nullable (need patch)
unnest($10::DECIMAL[]),
unnest($11::DECIMAL[]),
unnest($12::INT[]), -- nullable (need patch)
unnest($13::INT[]), -- nullable (need patch)
unnest($14::INT[]), -- nullable (need patch)
unnest($15::INT[]), -- nullable (need patch)
unnest($16::BOOLEAN[]), -- nullable (need patch)
unnest($17::JSONB[]),
unnest($18::TEXT[]), -- nullable (need patch)
unnest($19::INT[]), -- nullable (need patch)
unnest($20::BOOLEAN[]),
unnest($21::INT[])
)
`
type BatchCreateRunestonesParams struct {
TxHashArr []string
BlockHeightArr []int32
EtchingArr []bool
EtchingDivisibilityArr []int16
EtchingPremineArr []pgtype.Numeric
EtchingRuneArr []string
EtchingSpacersArr []int32
EtchingSymbolArr []int32
EtchingTermsArr []bool
EtchingTermsAmountArr []pgtype.Numeric
EtchingTermsCapArr []pgtype.Numeric
EtchingTermsHeightStartArr []int32
EtchingTermsHeightEndArr []int32
EtchingTermsOffsetStartArr []int32
EtchingTermsOffsetEndArr []int32
EtchingTurboArr []bool
EdictsArr [][]byte
MintArr []string
PointerArr []int32
CenotaphArr []bool
FlawsArr []int32
}
func (q *Queries) BatchCreateRunestones(ctx context.Context, arg BatchCreateRunestonesParams) error {
_, err := q.db.Exec(ctx, batchCreateRunestones,
arg.TxHashArr,
arg.BlockHeightArr,
arg.EtchingArr,
arg.EtchingDivisibilityArr,
arg.EtchingPremineArr,
arg.EtchingRuneArr,
arg.EtchingSpacersArr,
arg.EtchingSymbolArr,
arg.EtchingTermsArr,
arg.EtchingTermsAmountArr,
arg.EtchingTermsCapArr,
arg.EtchingTermsHeightStartArr,
arg.EtchingTermsHeightEndArr,
arg.EtchingTermsOffsetStartArr,
arg.EtchingTermsOffsetEndArr,
arg.EtchingTurboArr,
arg.EdictsArr,
arg.MintArr,
arg.PointerArr,
arg.CenotaphArr,
arg.FlawsArr,
)
return err
}
const batchSpendOutpointBalances = `-- name: BatchSpendOutpointBalances :exec
UPDATE runes_outpoint_balances
SET "spent_height" = $1::INT
FROM (
SELECT
unnest($2::TEXT[]) AS tx_hash,
unnest($3::INT[]) AS tx_idx
) AS input
WHERE "runes_outpoint_balances"."tx_hash" = "input"."tx_hash" AND "runes_outpoint_balances"."tx_idx" = "input"."tx_idx"
`
type BatchSpendOutpointBalancesParams struct {
SpentHeight int32
TxHashArr []string
TxIdxArr []int32
}
func (q *Queries) BatchSpendOutpointBalances(ctx context.Context, arg BatchSpendOutpointBalancesParams) error {
_, err := q.db.Exec(ctx, batchSpendOutpointBalances, arg.SpentHeight, arg.TxHashArr, arg.TxIdxArr)
return err
}

View File

@@ -0,0 +1,118 @@
package gen
import (
"context"
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgtype"
)
type BatchCreateRuneEntriesPatchedParams struct {
BatchCreateRuneEntriesParams
TermsHeightStartArr []pgtype.Int4
TermsHeightEndArr []pgtype.Int4
TermsOffsetStartArr []pgtype.Int4
TermsOffsetEndArr []pgtype.Int4
}
func (q *Queries) BatchCreateRuneEntriesPatched(ctx context.Context, arg BatchCreateRuneEntriesPatchedParams) error {
_, err := q.db.Exec(ctx, batchCreateRuneEntries,
arg.RuneIDArr,
arg.RuneArr,
arg.NumberArr,
arg.SpacersArr,
arg.PremineArr,
arg.SymbolArr,
arg.DivisibilityArr,
arg.TermsArr,
arg.TermsAmountArr,
arg.TermsCapArr,
arg.TermsHeightStartArr,
arg.TermsHeightEndArr,
arg.TermsOffsetStartArr,
arg.TermsOffsetEndArr,
arg.TurboArr,
arg.EtchingBlockArr,
arg.EtchingTxHashArr,
arg.EtchedAtArr,
)
return errors.WithStack(err)
}
type BatchCreateRuneEntryStatesPatchedParams struct {
BatchCreateRuneEntryStatesParams
CompletedAtHeightArr []pgtype.Int4
}
func (q *Queries) BatchCreateRuneEntryStatesPatched(ctx context.Context, arg BatchCreateRuneEntryStatesPatchedParams) error {
_, err := q.db.Exec(ctx, batchCreateRuneEntryStates,
arg.RuneIDArr,
arg.BlockHeightArr,
arg.MintsArr,
arg.BurnedAmountArr,
arg.CompletedAtArr,
arg.CompletedAtHeightArr,
)
return errors.WithStack(err)
}
type BatchCreateRunesOutpointBalancesPatchedParams struct {
BatchCreateRunesOutpointBalancesParams
SpentHeightArr []pgtype.Int4
}
func (q *Queries) BatchCreateRunesOutpointBalancesPatched(ctx context.Context, arg BatchCreateRunesOutpointBalancesPatchedParams) error {
_, err := q.db.Exec(ctx, batchCreateRunesOutpointBalances,
arg.RuneIDArr,
arg.PkscriptArr,
arg.TxHashArr,
arg.TxIdxArr,
arg.AmountArr,
arg.BlockHeightArr,
arg.SpentHeightArr,
)
return errors.WithStack(err)
}
type BatchCreateRunestonesPatchedParams struct {
BatchCreateRunestonesParams
EtchingDivisibilityArr []pgtype.Int2
EtchingRuneArr []pgtype.Text
EtchingSpacersArr []pgtype.Int4
EtchingSymbolArr []pgtype.Int4
EtchingTermsArr []pgtype.Bool
EtchingTermsHeightStartArr []pgtype.Int4
EtchingTermsHeightEndArr []pgtype.Int4
EtchingTermsOffsetStartArr []pgtype.Int4
EtchingTermsOffsetEndArr []pgtype.Int4
EtchingTurboArr []pgtype.Bool
MintArr []pgtype.Text
PointerArr []pgtype.Int4
}
func (q *Queries) BatchCreateRunestonesPatched(ctx context.Context, arg BatchCreateRunestonesPatchedParams) error {
_, err := q.db.Exec(ctx, batchCreateRunestones,
arg.TxHashArr,
arg.BlockHeightArr,
arg.EtchingArr,
arg.EtchingDivisibilityArr,
arg.EtchingPremineArr,
arg.EtchingRuneArr,
arg.EtchingSpacersArr,
arg.EtchingSymbolArr,
arg.EtchingTermsArr,
arg.EtchingTermsAmountArr,
arg.EtchingTermsCapArr,
arg.EtchingTermsHeightStartArr,
arg.EtchingTermsHeightEndArr,
arg.EtchingTermsOffsetStartArr,
arg.EtchingTermsOffsetEndArr,
arg.EtchingTurboArr,
arg.EdictsArr,
arg.MintArr,
arg.PointerArr,
arg.CenotaphArr,
arg.FlawsArr,
)
return errors.WithStack(err)
}

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,6 +45,54 @@ func (q *Queries) CreateIndexedBlock(ctx context.Context, arg CreateIndexedBlock
return err
}
const createOutPointBalance = `-- name: CreateOutPointBalance :exec
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)
`
type CreateOutPointBalanceParams struct {
RuneID string
Pkscript string
TxHash string
TxIdx int32
Amount pgtype.Numeric
BlockHeight int32
SpentHeight pgtype.Int4
}
func (q *Queries) CreateOutPointBalance(ctx context.Context, arg CreateOutPointBalanceParams) error {
_, err := q.db.Exec(ctx, createOutPointBalance,
arg.RuneID,
arg.Pkscript,
arg.TxHash,
arg.TxIdx,
arg.Amount,
arg.BlockHeight,
arg.SpentHeight,
)
return err
}
const createRuneBalance = `-- name: CreateRuneBalance :exec
INSERT INTO runes_balances (pkscript, block_height, rune_id, amount) VALUES ($1, $2, $3, $4)
`
type CreateRuneBalanceParams struct {
Pkscript string
BlockHeight int32
RuneID string
Amount pgtype.Numeric
}
func (q *Queries) CreateRuneBalance(ctx context.Context, arg CreateRuneBalanceParams) error {
_, err := q.db.Exec(ctx, createRuneBalance,
arg.Pkscript,
arg.BlockHeight,
arg.RuneID,
arg.Amount,
)
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)
@@ -428,6 +476,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 +627,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,18 +1232,55 @@ func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetR
return items, nil
}
const spendOutPointBalances = `-- name: SpendOutPointBalances :exec
const getTotalHoldersByRuneIds = `-- name: GetTotalHoldersByRuneIds :many
WITH balances AS (
SELECT DISTINCT ON (rune_id, pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = ANY($1::TEXT[]) AND block_height <= $2 ORDER BY rune_id, pkscript, block_height DESC
)
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id
`
type GetTotalHoldersByRuneIdsParams struct {
RuneIds []string
BlockHeight int32
}
type GetTotalHoldersByRuneIdsRow struct {
RuneID string
Count int64
}
func (q *Queries) GetTotalHoldersByRuneIds(ctx context.Context, arg GetTotalHoldersByRuneIdsParams) ([]GetTotalHoldersByRuneIdsRow, error) {
rows, err := q.db.Query(ctx, getTotalHoldersByRuneIds, arg.RuneIds, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTotalHoldersByRuneIdsRow
for rows.Next() {
var i GetTotalHoldersByRuneIdsRow
if err := rows.Scan(&i.RuneID, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const spendOutPointBalance = `-- name: SpendOutPointBalance :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
`
type SpendOutPointBalancesParams struct {
type SpendOutPointBalanceParams struct {
SpentHeight pgtype.Int4
TxHash string
TxIdx int32
}
func (q *Queries) SpendOutPointBalances(ctx context.Context, arg SpendOutPointBalancesParams) error {
_, err := q.db.Exec(ctx, spendOutPointBalances, arg.SpentHeight, arg.TxHash, arg.TxIdx)
func (q *Queries) SpendOutPointBalance(ctx context.Context, arg SpendOutPointBalanceParams) error {
_, err := q.db.Exec(ctx, spendOutPointBalance, arg.SpentHeight, arg.TxHash, arg.TxIdx)
return err
}

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// sqlc v1.27.0
package gen
@@ -15,7 +15,6 @@ type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
}
func New(db DBTX) *Queries {

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

@@ -11,6 +11,7 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/repository/postgres/gen"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
@@ -47,7 +48,7 @@ func numericFromUint128(src *uint128.Uint128) (pgtype.Numeric, error) {
func mapIndexerStateModelToType(src gen.RunesIndexerState) entity.IndexerState {
var createdAt time.Time
if src.CreatedAt.Valid {
createdAt = src.CreatedAt.Time
createdAt = src.CreatedAt.Time.UTC()
}
return entity.IndexerState{
DBVersion: src.DbVersion,
@@ -63,7 +64,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")
@@ -86,7 +87,7 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntr
}
var completedAt time.Time
if src.CompletedAt.Valid {
completedAt = src.CompletedAt.Time
completedAt = src.CompletedAt.Time.UTC()
}
var completedAtHeight *uint64
if src.CompletedAtHeight.Valid {
@@ -132,7 +133,7 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntr
}
var etchedAt time.Time
if src.EtchedAt.Valid {
etchedAt = src.EtchedAt.Time
etchedAt = src.EtchedAt.Time.UTC()
}
return runes.RuneEntry{
RuneId: runeId,
@@ -153,31 +154,13 @@ 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) (gen.CreateRuneEntryParams, 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")
}
burnedAmount, err := numericFromUint128(&src.BurnedAmount)
if err != nil {
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, 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")
}
var completedAt pgtype.Timestamp
if !src.CompletedAt.IsZero() {
completedAt.Time = src.CompletedAt
completedAt.Valid = true
}
var completedAtHeight pgtype.Int4
if src.CompletedAtHeight != nil {
completedAtHeight.Int32 = int32(*src.CompletedAtHeight)
completedAtHeight.Valid = true
return gen.CreateRuneEntryParams{}, errors.Wrap(err, "failed to parse premine")
}
var terms bool
var termsAmount, termsCap pgtype.Numeric
@@ -187,13 +170,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.CreateRuneEntryParams{}, 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.CreateRuneEntryParams{}, errors.Wrap(err, "failed to parse terms cap")
}
}
if src.Terms.HeightStart != nil {
@@ -221,51 +204,150 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.Crea
}
}
}
etchedAt := pgtype.Timestamp{Time: time.Time{}, Valid: true}
etchedAt := pgtype.Timestamp{Time: src.EtchedAt.UTC(), Valid: true}
return gen.CreateRuneEntryParams{
RuneID: runeId,
Rune: rune,
Number: int64(src.Number),
Spacers: spacers,
Premine: premine,
Symbol: src.Symbol,
Divisibility: int16(src.Divisibility),
Terms: terms,
TermsAmount: termsAmount,
TermsCap: termsCap,
TermsHeightStart: termsHeightStart,
TermsHeightEnd: termsHeightEnd,
TermsOffsetStart: termsOffsetStart,
TermsOffsetEnd: termsOffsetEnd,
Turbo: src.Turbo,
EtchingBlock: int32(src.EtchingBlock),
EtchingTxHash: src.EtchingTxHash.String(),
EtchedAt: etchedAt,
}, gen.CreateRuneEntryStateParams{
BlockHeight: int32(blockHeight),
RuneID: runeId,
Mints: mints,
BurnedAmount: burnedAmount,
CompletedAt: completedAt,
CompletedAtHeight: completedAtHeight,
}, nil
RuneID: runeId,
Rune: rune,
Number: int64(src.Number),
Spacers: spacers,
Premine: premine,
Symbol: src.Symbol,
Divisibility: int16(src.Divisibility),
Terms: terms,
TermsAmount: termsAmount,
TermsCap: termsCap,
TermsHeightStart: termsHeightStart,
TermsHeightEnd: termsHeightEnd,
TermsOffsetStart: termsOffsetStart,
TermsOffsetEnd: termsOffsetEnd,
Turbo: src.Turbo,
EtchingBlock: int32(src.EtchingBlock),
EtchingTxHash: src.EtchingTxHash.String(),
EtchedAt: etchedAt,
}, nil
}
// mapRuneTransactionModelToType returns params for creating a new rune transaction and (optionally) a runestone.
func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneTransactionParams, *gen.CreateRunestoneParams, error) {
func mapRuneEntryStatesTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.CreateRuneEntryStateParams, error) {
runeId := src.RuneId.String()
mints, err := numericFromUint128(&src.Mints)
if err != nil {
return gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse mints")
}
burnedAmount, err := numericFromUint128(&src.BurnedAmount)
if err != nil {
return gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse burned amount")
}
var completedAt pgtype.Timestamp
if !src.CompletedAt.IsZero() {
completedAt.Time = src.CompletedAt.UTC()
completedAt.Valid = true
}
var completedAtHeight pgtype.Int4
if src.CompletedAtHeight != nil {
completedAtHeight.Int32 = int32(*src.CompletedAtHeight)
completedAtHeight.Valid = true
}
return gen.CreateRuneEntryStateParams{
BlockHeight: int32(blockHeight),
RuneID: runeId,
Mints: mints,
BurnedAmount: burnedAmount,
CompletedAt: completedAt,
CompletedAtHeight: completedAtHeight,
}, nil
}
func mapRuneEntryTypeToParamsBatch(srcs []*runes.RuneEntry) (gen.BatchCreateRuneEntriesPatchedParams, error) {
var batchParams gen.BatchCreateRuneEntriesPatchedParams
batchParams.RuneIDArr = make([]string, 0, len(srcs))
batchParams.RuneArr = make([]string, 0, len(srcs))
batchParams.NumberArr = make([]int64, 0, len(srcs))
batchParams.SpacersArr = make([]int32, 0, len(srcs))
batchParams.PremineArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.SymbolArr = make([]int32, 0, len(srcs))
batchParams.DivisibilityArr = make([]int16, 0, len(srcs))
batchParams.TermsArr = make([]bool, 0, len(srcs))
batchParams.TermsAmountArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.TermsCapArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.TermsHeightStartArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.TermsHeightEndArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.TermsOffsetStartArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.TermsOffsetEndArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.TurboArr = make([]bool, 0, len(srcs))
batchParams.EtchingBlockArr = make([]int32, 0, len(srcs))
batchParams.EtchingTxHashArr = make([]string, 0, len(srcs))
batchParams.EtchedAtArr = make([]pgtype.Timestamp, 0, len(srcs))
for i, src := range srcs {
param, err := mapRuneEntryTypeToParams(*src)
if err != nil {
return gen.BatchCreateRuneEntriesPatchedParams{}, errors.Wrapf(err, "failed to map rune entry to params batch at index %d", i)
}
batchParams.RuneIDArr = append(batchParams.RuneIDArr, param.RuneID)
batchParams.RuneArr = append(batchParams.RuneArr, param.Rune)
batchParams.NumberArr = append(batchParams.NumberArr, param.Number)
batchParams.SpacersArr = append(batchParams.SpacersArr, param.Spacers)
batchParams.PremineArr = append(batchParams.PremineArr, param.Premine)
batchParams.SymbolArr = append(batchParams.SymbolArr, param.Symbol)
batchParams.DivisibilityArr = append(batchParams.DivisibilityArr, param.Divisibility)
batchParams.TermsArr = append(batchParams.TermsArr, param.Terms)
batchParams.TermsAmountArr = append(batchParams.TermsAmountArr, param.TermsAmount)
batchParams.TermsCapArr = append(batchParams.TermsCapArr, param.TermsCap)
batchParams.TermsHeightStartArr = append(batchParams.TermsHeightStartArr, param.TermsHeightStart)
batchParams.TermsHeightEndArr = append(batchParams.TermsHeightEndArr, param.TermsHeightEnd)
batchParams.TermsOffsetStartArr = append(batchParams.TermsOffsetStartArr, param.TermsOffsetStart)
batchParams.TermsOffsetEndArr = append(batchParams.TermsOffsetEndArr, param.TermsOffsetEnd)
batchParams.TurboArr = append(batchParams.TurboArr, param.Turbo)
batchParams.EtchingBlockArr = append(batchParams.EtchingBlockArr, param.EtchingBlock)
batchParams.EtchingTxHashArr = append(batchParams.EtchingTxHashArr, param.EtchingTxHash)
batchParams.EtchedAtArr = append(batchParams.EtchedAtArr, param.EtchedAt)
}
return batchParams, nil
}
func mapRuneEntryStatesTypeToParamsBatch(srcs []*runes.RuneEntry, blockHeight uint64) (gen.BatchCreateRuneEntryStatesPatchedParams, error) {
var batchParams gen.BatchCreateRuneEntryStatesPatchedParams
batchParams.RuneIDArr = make([]string, 0, len(srcs))
batchParams.BlockHeightArr = make([]int32, 0, len(srcs))
batchParams.MintsArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.BurnedAmountArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.CompletedAtArr = make([]pgtype.Timestamp, 0, len(srcs))
batchParams.CompletedAtHeightArr = make([]pgtype.Int4, 0, len(srcs))
for i, src := range srcs {
param, err := mapRuneEntryStatesTypeToParams(*src, blockHeight)
if err != nil {
return gen.BatchCreateRuneEntryStatesPatchedParams{}, errors.Wrapf(err, "failed to map rune entry states to params batch at index %d", i)
}
batchParams.RuneIDArr = append(batchParams.RuneIDArr, param.RuneID)
batchParams.BlockHeightArr = append(batchParams.BlockHeightArr, param.BlockHeight)
batchParams.MintsArr = append(batchParams.MintsArr, param.Mints)
batchParams.BurnedAmountArr = append(batchParams.BurnedAmountArr, param.BurnedAmount)
batchParams.CompletedAtArr = append(batchParams.CompletedAtArr, param.CompletedAt)
batchParams.CompletedAtHeightArr = append(batchParams.CompletedAtHeightArr, param.CompletedAtHeight)
}
return batchParams, nil
}
func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneTransactionParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp.Time = src.Timestamp
timestamp.Time = src.Timestamp.UTC()
timestamp.Valid = true
}
inputsBytes, err := json.Marshal(src.Inputs)
if err != nil {
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal inputs")
return gen.CreateRuneTransactionParams{}, 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.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal outputs")
}
mints := make(map[string]uint128.Uint128)
for key, value := range src.Mints {
@@ -273,7 +355,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.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal mints")
}
burns := make(map[string]uint128.Uint128)
for key, value := range src.Burns {
@@ -281,16 +363,7 @@ 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")
}
var runestoneParams *gen.CreateRunestoneParams
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")
}
runestoneParams = &params
return gen.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal burns")
}
return gen.CreateRuneTransactionParams{
@@ -303,7 +376,46 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
Mints: mintsBytes,
Burns: burnsBytes,
RuneEtched: src.RuneEtched,
}, runestoneParams, nil
}, nil
}
func mapRuneTransactionTypeToParamsBatch(srcs []*entity.RuneTransaction) (gen.BatchCreateRuneTransactionsParams, error) {
batchParams := gen.BatchCreateRuneTransactionsParams{
HashArr: make([]string, 0, len(srcs)),
BlockHeightArr: make([]int32, 0, len(srcs)),
IndexArr: make([]int32, 0, len(srcs)),
TimestampArr: make([]pgtype.Timestamp, 0, len(srcs)),
RuneEtchedArr: make([]bool, 0, len(srcs)),
}
inputsArr := make([][]byte, 0, len(srcs))
outputsArr := make([][]byte, 0, len(srcs))
mintsArr := make([][]byte, 0, len(srcs))
burnsArr := make([][]byte, 0, len(srcs))
for i, src := range srcs {
param, err := mapRuneTransactionTypeToParams(*src)
if err != nil {
return gen.BatchCreateRuneTransactionsParams{}, errors.Wrapf(err, "failed to map rune transaction to params batch at index %d", i)
}
batchParams.HashArr = append(batchParams.HashArr, param.Hash)
batchParams.BlockHeightArr = append(batchParams.BlockHeightArr, param.BlockHeight)
batchParams.IndexArr = append(batchParams.IndexArr, param.Index)
batchParams.TimestampArr = append(batchParams.TimestampArr, param.Timestamp)
batchParams.RuneEtchedArr = append(batchParams.RuneEtchedArr, param.RuneEtched)
inputsArr = append(inputsArr, param.Inputs)
outputsArr = append(outputsArr, param.Outputs)
mintsArr = append(mintsArr, param.Mints)
burnsArr = append(burnsArr, param.Burns)
}
batchParams.InputsArr = inputsArr
batchParams.OutputsArr = outputsArr
batchParams.MintsArr = mintsArr
batchParams.BurnsArr = burnsArr
return batchParams, nil
}
func extractModelRuneTxAndRunestone(src gen.GetRuneTransactionsRow) (gen.RunesTransaction, *gen.RunesRunestone, error) {
@@ -488,6 +600,65 @@ func mapRunestoneTypeToParams(src runes.Runestone, txHash chainhash.Hash, blockH
return runestoneParams, nil
}
func mapRunestoneTypeToParamsBatch(srcs []*entity.RuneTransaction) (gen.BatchCreateRunestonesPatchedParams, error) {
var batchParams gen.BatchCreateRunestonesPatchedParams
batchParams.TxHashArr = make([]string, 0, len(srcs))
batchParams.BlockHeightArr = make([]int32, 0, len(srcs))
batchParams.EtchingArr = make([]bool, 0, len(srcs))
batchParams.EtchingDivisibilityArr = make([]pgtype.Int2, 0, len(srcs))
batchParams.EtchingPremineArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.EtchingRuneArr = make([]pgtype.Text, 0, len(srcs))
batchParams.EtchingSpacersArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingSymbolArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingTermsArr = make([]pgtype.Bool, 0, len(srcs))
batchParams.EtchingTermsAmountArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.EtchingTermsCapArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.EtchingTermsHeightStartArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingTermsHeightEndArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingTermsOffsetStartArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingTermsOffsetEndArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.EtchingTurboArr = make([]pgtype.Bool, 0, len(srcs))
batchParams.EdictsArr = make([][]byte, 0, len(srcs))
batchParams.MintArr = make([]pgtype.Text, 0, len(srcs))
batchParams.PointerArr = make([]pgtype.Int4, 0, len(srcs))
batchParams.CenotaphArr = make([]bool, 0, len(srcs))
batchParams.FlawsArr = make([]int32, 0, len(srcs))
for i, src := range srcs {
if src.Runestone == nil {
continue
}
param, err := mapRunestoneTypeToParams(*src.Runestone, src.Hash, src.BlockHeight)
if err != nil {
return gen.BatchCreateRunestonesPatchedParams{}, errors.Wrapf(err, "failed to map runestone to params batch at index %d", i)
}
batchParams.TxHashArr = append(batchParams.TxHashArr, param.TxHash)
batchParams.BlockHeightArr = append(batchParams.BlockHeightArr, param.BlockHeight)
batchParams.EtchingArr = append(batchParams.EtchingArr, param.Etching)
batchParams.EtchingDivisibilityArr = append(batchParams.EtchingDivisibilityArr, param.EtchingDivisibility)
batchParams.EtchingPremineArr = append(batchParams.EtchingPremineArr, param.EtchingPremine)
batchParams.EtchingRuneArr = append(batchParams.EtchingRuneArr, param.EtchingRune)
batchParams.EtchingSpacersArr = append(batchParams.EtchingSpacersArr, param.EtchingSpacers)
batchParams.EtchingSymbolArr = append(batchParams.EtchingSymbolArr, param.EtchingSymbol)
batchParams.EtchingTermsArr = append(batchParams.EtchingTermsArr, param.EtchingTerms)
batchParams.EtchingTermsAmountArr = append(batchParams.EtchingTermsAmountArr, param.EtchingTermsAmount)
batchParams.EtchingTermsCapArr = append(batchParams.EtchingTermsCapArr, param.EtchingTermsCap)
batchParams.EtchingTermsHeightStartArr = append(batchParams.EtchingTermsHeightStartArr, param.EtchingTermsHeightStart)
batchParams.EtchingTermsHeightEndArr = append(batchParams.EtchingTermsHeightEndArr, param.EtchingTermsHeightEnd)
batchParams.EtchingTermsOffsetStartArr = append(batchParams.EtchingTermsOffsetStartArr, param.EtchingTermsOffsetStart)
batchParams.EtchingTermsOffsetEndArr = append(batchParams.EtchingTermsOffsetEndArr, param.EtchingTermsOffsetEnd)
batchParams.EtchingTurboArr = append(batchParams.EtchingTurboArr, param.EtchingTurbo)
batchParams.EdictsArr = append(batchParams.EdictsArr, param.Edicts)
batchParams.MintArr = append(batchParams.MintArr, param.Mint)
batchParams.PointerArr = append(batchParams.PointerArr, param.Pointer)
batchParams.CenotaphArr = append(batchParams.CenotaphArr, param.Cenotaph)
batchParams.FlawsArr = append(batchParams.FlawsArr, param.Flaws)
}
return batchParams, nil
}
func mapRunestoneModelToType(src gen.RunesRunestone) (runes.Runestone, error) {
runestone := runes.Runestone{
Cenotaph: src.Cenotaph,
@@ -602,6 +773,42 @@ func mapBalanceModelToType(src gen.RunesBalance) (*entity.Balance, error) {
}, nil
}
func mapBalanceTypeToParams(src entity.Balance) (gen.CreateRuneBalanceParams, error) {
amount, err := numericFromUint128(&src.Amount)
if err != nil {
return gen.CreateRuneBalanceParams{}, errors.Wrap(err, "failed to parse amount")
}
return gen.CreateRuneBalanceParams{
RuneID: src.RuneId.String(),
Amount: amount,
Pkscript: hex.EncodeToString(src.PkScript),
BlockHeight: int32(src.BlockHeight),
}, nil
}
func mapBalanceTypeToParamsBatch(srcs []*entity.Balance) (gen.BatchCreateRunesBalancesParams, error) {
batchParams := gen.BatchCreateRunesBalancesParams{
RuneIDArr: make([]string, 0, len(srcs)),
AmountArr: make([]pgtype.Numeric, 0, len(srcs)),
PkscriptArr: make([]string, 0, len(srcs)),
BlockHeightArr: make([]int32, 0, len(srcs)),
}
for i, src := range srcs {
param, err := mapBalanceTypeToParams(*src)
if err != nil {
return gen.BatchCreateRunesBalancesParams{}, errors.Wrapf(err, "failed to map balance to params batch at index %d", i)
}
batchParams.RuneIDArr = append(batchParams.RuneIDArr, param.RuneID)
batchParams.AmountArr = append(batchParams.AmountArr, param.Amount)
batchParams.PkscriptArr = append(batchParams.PkscriptArr, param.Pkscript)
batchParams.BlockHeightArr = append(batchParams.BlockHeightArr, param.BlockHeight)
}
return batchParams, nil
}
func mapIndexedBlockModelToType(src gen.RunesIndexedBlock) (*entity.IndexedBlock, error) {
hash, err := chainhash.NewHashFromStr(src.Hash)
if err != nil {
@@ -738,16 +945,16 @@ func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPoin
}, nil
}
func mapOutPointBalanceTypeToParams(src entity.OutPointBalance) (gen.CreateOutPointBalancesParams, error) {
func mapOutPointBalanceTypeToParams(src entity.OutPointBalance) (gen.CreateOutPointBalanceParams, error) {
amount, err := numericFromUint128(&src.Amount)
if err != nil {
return gen.CreateOutPointBalancesParams{}, errors.Wrap(err, "failed to parse amount")
return gen.CreateOutPointBalanceParams{}, errors.Wrap(err, "failed to parse amount")
}
var spentHeight pgtype.Int4
if src.SpentHeight != nil {
spentHeight = pgtype.Int4{Int32: int32(*src.SpentHeight), Valid: true}
}
return gen.CreateOutPointBalancesParams{
return gen.CreateOutPointBalanceParams{
TxHash: src.OutPoint.Hash.String(),
TxIdx: int32(src.OutPoint.Index),
Pkscript: hex.EncodeToString(src.PkScript),
@@ -757,3 +964,31 @@ func mapOutPointBalanceTypeToParams(src entity.OutPointBalance) (gen.CreateOutPo
SpentHeight: spentHeight,
}, nil
}
func mapOutPointBalanceTypeToParamsBatch(srcs []*entity.OutPointBalance) (gen.BatchCreateRunesOutpointBalancesPatchedParams, error) {
var batchParams gen.BatchCreateRunesOutpointBalancesPatchedParams
batchParams.TxHashArr = make([]string, 0, len(srcs))
batchParams.TxIdxArr = make([]int32, 0, len(srcs))
batchParams.PkscriptArr = make([]string, 0, len(srcs))
batchParams.RuneIDArr = make([]string, 0, len(srcs))
batchParams.AmountArr = make([]pgtype.Numeric, 0, len(srcs))
batchParams.BlockHeightArr = make([]int32, 0, len(srcs))
batchParams.SpentHeightArr = make([]pgtype.Int4, 0, len(srcs))
for i, src := range srcs {
param, err := mapOutPointBalanceTypeToParams(*src)
if err != nil {
return gen.BatchCreateRunesOutpointBalancesPatchedParams{}, errors.Wrapf(err, "failed to map outpoint balance to params batch at index %d", i)
}
batchParams.TxHashArr = append(batchParams.TxHashArr, param.TxHash)
batchParams.TxIdxArr = append(batchParams.TxIdxArr, param.TxIdx)
batchParams.PkscriptArr = append(batchParams.PkscriptArr, param.Pkscript)
batchParams.RuneIDArr = append(batchParams.RuneIDArr, param.RuneID)
batchParams.AmountArr = append(batchParams.AmountArr, param.Amount)
batchParams.BlockHeightArr = append(batchParams.BlockHeightArr, param.BlockHeight)
batchParams.SpentHeightArr = append(batchParams.SpentHeightArr, param.SpentHeight)
}
return batchParams, nil
}

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,111 +459,135 @@ 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 {
return nil
}
txParams, runestoneParams, err := mapRuneTransactionTypeToParams(*tx)
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 errors.Wrap(err, "failed to map rune transaction to params")
return nil, errors.Wrap(err, "error during query")
}
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")
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, err := mapRuneTransactionTypeToParamsBatch(txs)
if err != nil {
return errors.Wrap(err, "failed to map rune transactions to params")
}
if err := r.queries.BatchCreateRuneTransactions(ctx, txParams); err != nil {
return errors.Wrap(err, "error during exec BatchCreateRuneTransactions")
}
runestoneParams, err := mapRunestoneTypeToParamsBatch(txs)
if err != nil {
return errors.Wrap(err, "failed to map runestones to params")
}
if err := r.queries.BatchCreateRunestonesPatched(ctx, runestoneParams); err != nil {
return errors.Wrap(err, "error during exec BatchCreateRunestones")
}
return nil
}
func (r *Repository) CreateRuneEntry(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
func (r *Repository) CreateRuneEntries(ctx context.Context, entries []*runes.RuneEntry) error {
if len(entries) == 0 {
return nil
}
createParams, _, err := mapRuneEntryTypeToParams(*entry, blockHeight)
params, err := mapRuneEntryTypeToParamsBatch(entries)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
return errors.Wrap(err, "failed to map rune entries to params")
}
if err = r.queries.CreateRuneEntry(ctx, createParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntry")
if err := r.queries.BatchCreateRuneEntriesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) CreateRuneEntryState(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
func (r *Repository) CreateRuneEntryStates(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error {
if len(entries) == 0 {
return nil
}
_, createStateParams, err := mapRuneEntryTypeToParams(*entry, blockHeight)
params, err := mapRuneEntryStatesTypeToParamsBatch(entries, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry to params")
return errors.Wrap(err, "failed to map rune entry states to params")
}
if err = r.queries.CreateRuneEntryState(ctx, createStateParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntryState")
if err := r.queries.BatchCreateRuneEntryStatesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) CreateOutPointBalances(ctx context.Context, outPointBalances []*entity.OutPointBalance) error {
params := make([]gen.CreateOutPointBalancesParams, 0, len(outPointBalances))
for _, balance := range outPointBalances {
param, err := mapOutPointBalanceTypeToParams(*balance)
if err != nil {
return errors.Wrap(err, "failed to map outpoint balance to params")
}
params = append(params, param)
if len(outPointBalances) == 0 {
return nil
}
result := r.queries.CreateOutPointBalances(ctx, params)
var execErrors []error
result.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
}
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 {
params, err := mapOutPointBalanceTypeToParamsBatch(outPointBalances)
if err != nil {
return errors.Wrap(err, "failed to map outpoint balances to params")
}
if err := r.queries.BatchCreateRunesOutpointBalancesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) CreateRuneBalances(ctx context.Context, params []datagateway.CreateRuneBalancesParams) error {
insertParams := make([]gen.CreateRuneBalanceAtBlockParams, 0, len(params))
for _, param := range params {
param := param
amount, err := numericFromUint128(&param.Balance)
if err != nil {
return errors.Wrap(err, "failed to convert balance to numeric")
}
insertParams = append(insertParams, gen.CreateRuneBalanceAtBlockParams{
Pkscript: hex.EncodeToString(param.PkScript),
BlockHeight: int32(param.BlockHeight),
RuneID: param.RuneId.String(),
Amount: amount,
})
func (r *Repository) SpendOutPointBalancesBatch(ctx context.Context, outPoints []wire.OutPoint, blockHeight uint64) error {
if len(outPoints) == 0 {
return nil
}
result := r.queries.CreateRuneBalanceAtBlock(ctx, insertParams)
var execErrors []error
result.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")
params := gen.BatchSpendOutpointBalancesParams{
TxHashArr: make([]string, 0, len(outPoints)),
TxIdxArr: make([]int32, 0, len(outPoints)),
SpentHeight: int32(blockHeight),
}
for _, outPoint := range outPoints {
params.TxHashArr = append(params.TxHashArr, outPoint.Hash.String())
params.TxIdxArr = append(params.TxIdxArr, int32(outPoint.Index))
}
if err := r.queries.BatchSpendOutpointBalances(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) CreateRuneBalances(ctx context.Context, balances []*entity.Balance) error {
if len(balances) == 0 {
return nil
}
params, err := mapBalanceTypeToParamsBatch(balances)
if err != nil {
return errors.Wrap(err, "failed to map rune balances to params")
}
if err := r.queries.BatchCreateRunesBalances(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}

View File

@@ -67,10 +67,11 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
}
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient, cleanupFuncs)
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
if !conf.APIOnly {
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
}
}
// Mount API
apiHandlers := lo.Uniq(conf.Modules.Runes.APIHandlers)
for _, handler := range apiHandlers {

View File

@@ -25,3 +25,21 @@ 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
}
func (u *Usecase) GetTotalHoldersByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (int64, error) {
holders, err := u.runesDg.GetTotalHoldersByRuneIds(ctx, []runes.RuneId{runeId}, blockHeight)
if err != nil {
return 0, errors.Wrap(err, "failed to get total holders by rune ids")
}
// defaults to zero holders if not found
return holders[runeId], nil
}

View File

@@ -40,9 +40,25 @@ func (u *Usecase) GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId rune
}
func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error) {
runeEntry, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
runeEntries, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "failed to get rune entries by rune ids and height")
}
return runeEntry, nil
return runeEntries, 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
}

191
pkg/bip322/bip322.go Normal file
View File

@@ -0,0 +1,191 @@
package bip322
// This package is forked from https://github.com/unisat-wallet/libbrc20-indexer/blob/v1.1.0/utils/bip322/verify.go,
// with a few modifications to make the interface more friendly with Gaze types.
import (
"crypto/sha256"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/pkg/btcutils"
)
func GetSha256(data []byte) (hash []byte) {
sha := sha256.New()
sha.Write(data[:])
hash = sha.Sum(nil)
return
}
func GetTagSha256(data []byte) (hash []byte) {
tag := []byte("BIP0322-signed-message")
hashTag := GetSha256(tag)
var msg []byte
msg = append(msg, hashTag...)
msg = append(msg, hashTag...)
msg = append(msg, data...)
return GetSha256(msg)
}
func PrepareTx(pkScript []byte, message string) (toSign *wire.MsgTx, err error) {
// Create a new transaction to spend
toSpend := wire.NewMsgTx(0)
// Decode the message hash
messageHash := GetTagSha256([]byte(message))
// Create the script for to_spend
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_0)
builder.AddData(messageHash)
scriptSig, err := builder.Script()
if err != nil {
return nil, errors.WithStack(err)
}
// Create a TxIn with the outpoint 000...000:FFFFFFFF
prevOutHash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
prevOut := wire.NewOutPoint(prevOutHash, wire.MaxPrevOutIndex)
txIn := wire.NewTxIn(prevOut, scriptSig, nil)
txIn.Sequence = 0
toSpend.AddTxIn(txIn)
toSpend.AddTxOut(wire.NewTxOut(0, pkScript))
// Create a transaction for to_sign
toSign = wire.NewMsgTx(0)
hash := toSpend.TxHash()
prevOutSpend := wire.NewOutPoint((*chainhash.Hash)(hash.CloneBytes()), 0)
txSignIn := wire.NewTxIn(prevOutSpend, nil, nil)
txSignIn.Sequence = 0
toSign.AddTxIn(txSignIn)
// Create the script for to_sign
builderPk := txscript.NewScriptBuilder()
builderPk.AddOp(txscript.OP_RETURN)
scriptPk, err := builderPk.Script()
if err != nil {
return nil, errors.WithStack(err)
}
toSign.AddTxOut(wire.NewTxOut(0, scriptPk))
return toSign, nil
}
func VerifyMessage(address *btcutils.Address, signature []byte, message string) bool {
if len(signature) == 0 {
// empty signature is invalid
return false
}
// BIP322 signature format is the serialized witness of the toSign transaction.
// [0x02] [SIGNATURE_LEN, ...(signature that go into witness[0])] [PUBLIC_KEY_LEN, ...(public key that was used to sign the message, go to witness[1])]
witness, err := DeserializeWitnessSignature(signature)
if err != nil {
// invalid signature
return false
}
return verifySignatureWitness(witness, address.ScriptPubKey(), message)
}
// verifySignatureWitness
// signature: 64B, pkScript: 33B, message: any
func verifySignatureWitness(witness wire.TxWitness, pkScript []byte, message string) bool {
toSign, err := PrepareTx(pkScript, message)
if err != nil {
return false
}
toSign.TxIn[0].Witness = witness
prevFetcher := txscript.NewCannedPrevOutputFetcher(
pkScript, 0,
)
hashCache := txscript.NewTxSigHashes(toSign, prevFetcher)
vm, err := txscript.NewEngine(pkScript, toSign, 0, txscript.StandardVerifyFlags, nil, hashCache, 0, prevFetcher)
if err != nil {
return false
}
if err := vm.Execute(); err != nil {
return false
}
return true
}
func SignMessage(privateKey *btcec.PrivateKey, address *btcutils.Address, message string) ([]byte, error) {
var witness wire.TxWitness
var err error
switch address.Type() {
case btcutils.AddressP2TR:
witness, _, err = SignSignatureTaproot(privateKey, message)
case btcutils.AddressP2WPKH:
witness, _, err = SignSignatureP2WPKH(privateKey, message)
}
if err != nil {
return nil, errors.WithStack(err)
}
signature, err := SerializeWitnessSignature(witness)
if err != nil {
return nil, errors.WithStack(err)
}
return signature, nil
}
func SignSignatureTaproot(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) {
pubKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey())
pkScript, err = PayToTaprootScript(pubKey)
if err != nil {
return nil, nil, errors.WithStack(err)
}
toSign, err := PrepareTx(pkScript, message)
if err != nil {
return nil, nil, errors.WithStack(err)
}
prevFetcher := txscript.NewCannedPrevOutputFetcher(
pkScript, 0,
)
sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher)
witness, err = txscript.TaprootWitnessSignature(
toSign, sigHashes, 0, 0, pkScript,
txscript.SigHashDefault, privKey,
)
if err != nil {
return nil, nil, errors.WithStack(err)
}
return witness, pkScript, nil
}
func SignSignatureP2WPKH(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) {
pubKey := privKey.PubKey()
pkScript, err = PayToWitnessScript(pubKey)
if err != nil {
return nil, nil, errors.WithStack(err)
}
toSign, err := PrepareTx(pkScript, message)
if err != nil {
return nil, nil, errors.WithStack(err)
}
prevFetcher := txscript.NewCannedPrevOutputFetcher(
pkScript, 0,
)
sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher)
witness, err = txscript.WitnessSignature(toSign, sigHashes,
0, 0, pkScript, txscript.SigHashAll,
privKey, true)
if err != nil {
return nil, nil, errors.WithStack(err)
}
return witness, pkScript, nil
}

145
pkg/bip322/bip322_test.go Normal file
View File

@@ -0,0 +1,145 @@
package bip322
import (
"encoding/base64"
"fmt"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifyMessage(t *testing.T) {
type testcase struct {
Address string
Message string
Signature string // base64
Expected bool
}
testcases := []testcase{
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "",
Signature: "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
Expected: true,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "",
Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy",
Expected: true,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "Hello World",
Signature: "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
Expected: true,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "Hello World",
Signature: "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy",
Expected: true,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "",
Signature: "INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVA",
Expected: false,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "",
Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX",
Expected: false,
},
{
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "Hello World",
Signature: "BkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX",
Expected: false,
},
{
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "",
Signature: "AUDVvVp7mCtPZtoORKYcMM+idx9yy5+z4TGeoI/PWEUscd5x0QYJ6IPQ/anBSMWPWSRPqHVrEjOIWhP9FsZSMFdG",
Expected: true,
},
{
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "",
Signature: "AUDYeG/k6AL9pNuhgK8aJqxIqBIObX867yc3QgdfS70sWEdUg0Msv0Ps24Pt5aQmcI2wZdwI3Egp5tA5PW+wTOw6",
Expected: true,
},
{
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "Hello World",
Signature: "AUCkOlzIYSN6T+QzENjlp61Pa2l4EyDDH8c4pFANOwoh3oGi/iZHscAExUSePhbS94KIMgcg+yNp+LsckO+AfLQQ",
Expected: true,
},
{
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "Hello World",
Signature: "AUD5MwxtURP3tAip3fS5vVRwa4L15wEyTIG0BQ3DPktJpXvQe7Sh8kf+mVaO4ldEP+vhiVZ/sXvOHEbQQnsiYpCq",
Expected: true,
},
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) {
address, err := btcutils.SafeNewAddress(tc.Address)
require.NoError(t, err)
signature, err := base64.StdEncoding.DecodeString(tc.Signature)
require.NoError(t, err)
verified := VerifyMessage(&address, signature, tc.Message)
assert.Equal(t, tc.Expected, verified)
})
}
}
func TestSignMessage(t *testing.T) {
type testcase struct {
PrivateKey *btcec.PrivateKey
Address string
Message string
}
testcases := []testcase{
{
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "",
},
{
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
Message: "Hello World",
},
{
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "",
},
{
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
Message: "Hello World",
},
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) {
address, err := btcutils.SafeNewAddress(tc.Address)
require.NoError(t, err)
signature, err := SignMessage(tc.PrivateKey, &address, tc.Message)
require.NoError(t, err)
verified := VerifyMessage(&address, signature, tc.Message)
assert.True(t, verified)
})
}
}

77
pkg/bip322/bip322_util.go Normal file
View File

@@ -0,0 +1,77 @@
package bip322
import (
"bytes"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
func SerializeWitnessSignature(witness wire.TxWitness) ([]byte, error) {
result := new(bytes.Buffer)
buf := make([]byte, 8)
if err := wire.WriteVarIntBuf(result, 0, uint64(len(witness)), buf); err != nil {
return nil, errors.WithStack(err)
}
for _, item := range witness {
if err := wire.WriteVarBytesBuf(result, 0, item, buf); err != nil {
return nil, errors.WithStack(err)
}
}
return result.Bytes(), nil
}
func DeserializeWitnessSignature(serialized []byte) (wire.TxWitness, error) {
if len(serialized) == 0 {
return nil, errors.Wrap(errs.ArgumentRequired, "serialized witness is required")
}
witness := make(wire.TxWitness, 0)
current := 0
witnessLen := int(serialized[current])
current++
for i := 0; i < witnessLen; i++ {
if current >= len(serialized) {
return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes")
}
witnessItemLen := int(serialized[current])
current++
if current+witnessItemLen > len(serialized) {
return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes")
}
witnessItem := serialized[current : current+witnessItemLen]
current += witnessItemLen
witness = append(witness, witnessItem)
}
return witness, nil
}
// PayToTaprootScript creates a pk script for a pay-to-taproot output key.
func PayToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) {
script, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_1).
AddData(schnorr.SerializePubKey(taprootKey)).
Script()
if err != nil {
return nil, errors.WithStack(err)
}
return script, nil
}
// PayToWitnessScript creates a pk script for a pay-to-wpkh output key.
func PayToWitnessScript(pubkey *btcec.PublicKey) ([]byte, error) {
script, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(btcutil.Hash160(pubkey.SerializeCompressed())).
Script()
if err != nil {
return nil, errors.WithStack(err)
}
return script, nil
}

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