Compare commits

..

29 Commits

Author SHA1 Message Date
Waris Aiemworawutikul
e5e742cfa5 fix: handle block timeout = 0 2024-07-31 12:16:26 +07:00
Waris Aiemworawutikul
10d31de197 fix: wrong timestamp format. 2024-07-30 14:45:02 +07:00
Waris Aiemworawutikul
eab0eb5ac3 feat: add extra unit test. 2024-07-26 18:05:54 +07:00
Waris Aiemworawutikul
1d2cc88f90 fix: change to using input values since output values deducted fee. 2024-07-26 17:57:43 +07:00
Waris Aiemworawutikul
8a1c096656 refactor: convert to unit tests. 2024-07-26 17:07:33 +07:00
Waris Aiemworawutikul
17ab11200a refactor: prepare for moving integration tests. 2024-07-26 12:01:11 +07:00
Waris Aiemworawutikul
1358b96165 fix: delegate tx hash not record into db. 2024-07-26 11:23:01 +07:00
Waris Aiemworawutikul
2c0a01da56 fix: refactor 2024-07-25 17:52:13 +07:00
Waris Aiemworawutikul
a5651f9812 fix: refactor 2024-07-25 17:09:50 +07:00
Waris Aiemworawutikul
df59e5fb46 fix: 2024-07-23 18:30:07 +07:00
Waris Aiemworawutikul
43a85c5171 fix: minor mistakes. 2024-07-23 18:04:34 +07:00
Waris Aiemworawutikul
5574db8691 fix: move last_block_default to config file. 2024-07-23 13:39:38 +07:00
Waris Aiemworawutikul
5497ef85c6 fix: remove unused code. 2024-07-19 19:17:17 +07:00
Waris Aiemworawutikul
28c42c02b7 fix: sanity refactor. 2024-07-19 19:05:06 +07:00
Waris Aiemworawutikul
38cf4c95a5 fix: more consistent SQL 2024-07-19 12:02:45 +07:00
Waris Aiemworawutikul
b8ed647d91 fix: 50% chance public key compare incorrectly. 2024-07-19 11:58:50 +07:00
Waris Aiemworawutikul
9f327c58fa fix: Cannot run nodesale test because qtx is not initiated. 2024-07-18 14:56:15 +07:00
Gaze
dd35ab125e feat: add todo note 2024-07-16 15:47:30 +07:00
Gaze
276b20abca fix: handle error 2024-07-16 15:41:40 +07:00
Gaze
d62445887b fix: remove os.Exit 2024-07-16 15:37:56 +07:00
Gaze
cbc6bde3b5 fix: try to skip test in ci 2024-07-16 15:31:54 +07:00
Gaze
f150451dd7 ci: use echo to create new file 2024-07-16 15:27:52 +07:00
Gaze
0b09724c71 ci: touch result file 2024-07-16 15:23:04 +07:00
Gaze
237d0f0d73 ci: try to tidy before testing 2024-07-16 15:09:02 +07:00
Waris Aiemworawutikul
b10b8c59d7 fix: bug UTC time. 2024-07-16 13:45:04 +07:00
Waris Aiemworawutikul
6be4c877d6 fix: add entity 2024-07-16 13:45:04 +07:00
Waris Aiemworawutikul
c86380718f fix: fix table type. 2024-07-16 13:45:04 +07:00
Waris Aiemworawutikul
1e462a09d9 fix: refactored. 2024-07-16 13:45:04 +07:00
Waris Aiemworawutikul
338d4d47b3 feat: recover nodesale module. 2024-07-16 13:45:04 +07:00
79 changed files with 761 additions and 4621 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup Sqlc
uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: "1.27.0"
sqlc-version: "1.26.0"
- name: Check Diff
run: sqlc diff

View File

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

View File

@@ -39,7 +39,7 @@
"ui.completion.usePlaceholders": false,
"ui.diagnostic.analyses": {
// https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md
"fieldalignment": false,
// "fieldalignment": false,
"nilness": true,
"shadow": false,
"unusedparams": true,

View File

@@ -1,7 +1,5 @@
<!-- omit from toc -->
- [Türkçe](https://github.com/Rumeyst/gaze-indexer/blob/turkish-translation/docs/README_tr.md)
# Gaze Indexer
Gaze Indexer is an open-source and modular indexing client for Bitcoin meta-protocols with **Unified Consistent APIs** across fungible token protocols.

View File

@@ -7,13 +7,13 @@ import (
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/constants"
"github.com/gaze-network/indexer-network/modules/nodesale"
runesconstants "github.com/gaze-network/indexer-network/modules/runes/constants"
"github.com/gaze-network/indexer-network/modules/runes"
"github.com/spf13/cobra"
)
var versions = map[string]string{
"": constants.Version,
"runes": runesconstants.Version,
"runes": runes.Version,
"nodesale": nodesale.Version,
}

4
common/bitcoin.go Normal file
View File

@@ -0,0 +1,4 @@
package common
// HalvingInterval is the number of blocks between each halving event.
const HalvingInterval = 210_000

View File

@@ -24,9 +24,6 @@ var (
// Skippable is returned when got an error but it can be skipped or ignored and continue
Skippable = errors.NewWithDepth(depth, "skippable")
// Retryable is returned when got an error but it can be retried
Retryable = errors.NewWithDepth(depth, "retryable")
// Unsupported is returned when a feature or result is not supported
Unsupported = errors.NewWithDepth(depth, "unsupported")

View File

@@ -11,7 +11,6 @@ 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 {
@@ -22,10 +21,6 @@ func (p PublicError) Message() string {
return p.message
}
func (p PublicError) Code() string {
return p.code
}
func (p PublicError) Unwrap() error {
return p.err
}
@@ -34,10 +29,6 @@ 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
@@ -50,16 +41,3 @@ 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)
}

View File

@@ -1,31 +1,22 @@
package common
import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/gaze-network/indexer-network/pkg/logger"
)
import "github.com/btcsuite/btcd/chaincfg"
type Network string
const (
NetworkMainnet Network = "mainnet"
NetworkTestnet Network = "testnet"
NetworkFractalMainnet Network = "fractal-mainnet"
NetworkFractalTestnet Network = "fractal-testnet"
NetworkMainnet Network = "mainnet"
NetworkTestnet Network = "testnet"
)
var supportedNetworks = map[Network]struct{}{
NetworkMainnet: {},
NetworkTestnet: {},
NetworkFractalMainnet: {},
NetworkFractalTestnet: {},
NetworkMainnet: {},
NetworkTestnet: {},
}
var chainParams = map[Network]*chaincfg.Params{
NetworkMainnet: &chaincfg.MainNetParams,
NetworkTestnet: &chaincfg.TestNet3Params,
NetworkFractalMainnet: &chaincfg.MainNetParams,
NetworkFractalTestnet: &chaincfg.MainNetParams,
NetworkMainnet: &chaincfg.MainNetParams,
NetworkTestnet: &chaincfg.TestNet3Params,
}
func (n Network) IsSupported() bool {
@@ -40,15 +31,3 @@ func (n Network) ChainParams() *chaincfg.Params {
func (n Network) String() string {
return string(n)
}
func (n Network) HalvingInterval() uint64 {
switch n {
case NetworkMainnet, NetworkTestnet:
return 210_000
case NetworkFractalMainnet, NetworkFractalTestnet:
return 2_100_000
default:
logger.Panic("invalid network")
return 0
}
}

View File

@@ -285,12 +285,3 @@ func (d *BitcoinNodeDatasource) GetBlockHeader(ctx context.Context, height int64
return types.ParseMsgBlockHeader(*block, height), nil
}
func (d *BitcoinNodeDatasource) GetRawTransactionByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, error) {
transaction, err := d.btcclient.GetRawTransaction(&txHash)
if err != nil {
return nil, errors.Wrap(err, "failed to get raw transaction")
}
return transaction.MsgTx(), nil
}

View File

@@ -6,7 +6,6 @@ import (
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
@@ -143,7 +142,7 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
// validate reorg from first input
{
remoteBlockHeader := firstInputHeader
if i.currentBlock.Hash != (chainhash.Hash{}) && !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
if !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
logger.WarnContext(ctx, "Detected chain reorganization. Searching for fork point...",
slogx.String("event", "reorg_detected"),
slogx.Stringer("current_hash", i.currentBlock.Hash),
@@ -216,7 +215,7 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
return errors.Wrapf(errs.InternalError, "input is not continuous, input[%d] height: %d, input[%d] height: %d", i-1, prevHeader.Height, i, header.Height)
}
if prevHeader.Hash != (chainhash.Hash{}) && !header.PrevBlock.IsEqual(&prevHeader.Hash) {
if !header.PrevBlock.IsEqual(&prevHeader.Hash) {
logger.WarnContext(ctx, "Chain Reorganization occurred in the middle of batch fetching inputs, need to try to fetch again")
// end current round

View File

@@ -1,165 +0,0 @@
## Çeviriler
- [English (İngilizce)](../README.md)
**Son Güncelleme:** 21 Ağustos 2024
> **Not:** Bu belge, topluluk tarafından yapılmış bir çeviridir. Ana README.md dosyasındaki güncellemeler buraya otomatik olarak yansıtılmayabilir. En güncel bilgiler için [İngilizce sürümü](../README.md) inceleyin.
# Gaze Indexer
Gaze Indexer, değiştirilebilir token protokolleri arasında **Birleştirilmiş Tutarlı API'lere** sahip Bitcoin meta-protokolleri için açık kaynaklı ve modüler bir indeksleme istemcisidir.
Gaze Indexer, kullanıcıların tüm modülleri tek bir komutla tek bir monolitik örnekte veya dağıtılmış bir mikro hizmet kümesi olarak çalıştırmasına olanak tanıyan **modülerlik** göz önünde bulundurularak oluşturulmuştur.
Gaze Indexer, verimli veri getirme, yeniden düzenleme algılama ve veritabanı taşıma aracı ile HERHANGİ bir meta-protokol indeksleyici oluşturmak için bir temel görevi görür.
Bu, geliştiricilerin **gerçekten** önemli olana odaklanmasını sağlar: Meta-protokol indeksleme mantığı. Yeni meta-protokoller, yeni modüller uygulanarak kolayca eklenebilir.
- [Modüller](#modules)
- [1. Runes](#1-runes)
- [Kurulum](#installation)
- [Önkoşullar](#prerequisites)
- [1. Donanım Gereksinimleri](#1-hardware-requirements)
- [2. Bitcoin Core RPC sunucusunu hazırlayın.](#2-prepare-bitcoin-core-rpc-server)
- [3. Veritabanı hazırlayın.](#3-prepare-database)
- [4. `config.yaml` dosyasını hazırlayın.](#4-prepare-configyaml-file)
- [Docker ile yükle (önerilir)](#install-with-docker-recommended)
- [Kaynaktan yükle](#install-from-source)
## Modüller
### 1. Runes
Runes Dizinleyici ilk meta-protokol dizinleyicimizdir. Bitcoin işlemlerini kullanarak Runes durumlarını, işlemlerini, rün taşlarını ve bakiyelerini indeksler.
Geçmiş Runes verilerini sorgulamak için bir dizi API ile birlikte gelir. Tüm ayrıntılar için [API Referansı] (https://api-docs.gaze.network) adresimize bakın.
## Kurulum
### Önkoşullar
#### 1. Donanım Gereksinimleri
Her modül farklı donanım gereksinimleri gerektirir.
| Modül | CPU | RAM |
| ------ | --------- | ---- |
| Runes | 0,5 çekirdek | 1 GB |
#### 2. Bitcoin Core RPC sunucusunu hazırlayın.
Gaze Indexer'ın işlem verilerini kendi barındırdığı ya da QuickNode gibi yönetilen sağlayıcıları kullanan bir Bitcoin Core RPC'den alması gerekir.
Bir Bitcoin Core'u kendiniz barındırmak için bkz. https://bitcoin.org/en/full-node.
#### 3. Veritabanını hazırlayın.
Gaze Indexer PostgreSQL için birinci sınıf desteğe sahiptir. Diğer veritabanlarını kullanmak isterseniz, her modülün Veri Ağ Geçidi arayüzünü karşılayan kendi veritabanı havuzunuzu uygulayabilirsiniz.
İşte her modül için minimum veritabanı disk alanı gereksinimimiz.
| Modül | Veritabanı Depolama Alanı (mevcut) | Veritabanı Depolama Alanı (1 yıl içinde) |
| ------ | -------------------------- | ---------------------------- |
| Runes | 10 GB | 150 GB |
#### 4. config.yaml` dosyasını hazırlayın.
```yaml
# config.yaml
logger:
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
debug: false
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
network: mainnet
# Bitcoin Core RPC configuration options.
bitcoin_node:
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
user: "" # Username to authenticate with Bitcoin Core RPC
pass: "" # Password to authenticate with Bitcoin Core RPC
disable_tls: false # Set to true to disable tls
# Block reporting configuration options. See Block Reporting section for more details.
reporting:
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
# HTTP server configuration options.
http_server:
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
# Meta-protocol modules configuration options.
modules:
# Configuration options for Runes module. Can be removed if not used.
runes:
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
datasource: "bitcoin-node" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
api_handlers: # API handlers to enable. current supported handlers: "http"
- http
postgres:
host: "localhost"
port: 5432
user: "postgres"
password: "password"
db_name: "postgres"
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
```
### Docker ile yükleyin (önerilir)
Kurulum kılavuzumuz için `docker-compose` kullanacağız. Docker-compose.yaml` dosyasının `config.yaml` dosyası ile aynı dizinde olduğundan emin olun.
```yaml
# docker-compose.yaml
services:
gaze-indexer:
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
container_name: gaze-indexer
restart: unless-stopped
ports:
- 8080:8080 # Expose HTTP server port to host
volumes:
- "./config.yaml:/app/config.yaml" # mount config.yaml file to the container as "/app/config.yaml"
command: ["/app/main", "run", "--modules", "runes"] # Put module flags after "run" commands to select which modules to run.
```
### Kaynaktan yükleyin
1. Go` sürüm 1.22 veya daha üstünü yükleyin. Go kurulum kılavuzuna bakın [burada](https://go.dev/doc/install).
2. Bu depoyu klonlayın.
```bash
git clone https://github.com/gaze-network/gaze-indexer.git
cd gaze-indexer
```
3. Ana ikili dosyayı oluşturun.
```bash
# Bağımlılıkları al
go mod indir
# Ana ikili dosyayı oluşturun
go build -o gaze main.go
```
4. Veritabanı geçişlerini `migrate` komutu ve modül bayrakları ile çalıştırın.
```bash
./gaze migrate up --runes --database postgres://postgres:password@localhost:5432/postgres
```
5. Dizinleyiciyi `run` komutu ve modül bayrakları ile başlatın.
```bash
./gaze run --modules runes
```
Eğer `config.yaml` dosyası `./app/config.yaml` adresinde bulunmuyorsa, `config.yaml` dosyasının yolunu belirtmek için `--config` bayrağını kullanın.
```bash
./gaze run --modules runes --config /path/to/config.yaml
```
## Çeviriler
- [English (İngilizce)](../README.md)

2
go.sum
View File

@@ -99,7 +99,6 @@ 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=
@@ -233,7 +232,6 @@ 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

@@ -6,7 +6,6 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gofiber/fiber/v2"
)
@@ -44,24 +43,15 @@ func (h *handler) nodesHandler(ctx *fiber.Ctx) error {
return errs.NewPublicError("Invalid deploy ID")
}
var nodes []entity.Node
if ownerPublicKey == "" {
nodes, err = h.nodeSaleDg.GetNodesByDeployment(ctx.UserContext(), blockHeight, txIndex)
if err != nil {
return errors.Wrap(err, "Can't get nodes from db")
}
} else {
nodes, err = h.nodeSaleDg.GetNodesByPubkey(ctx.UserContext(), datagateway.GetNodesByPubkeyParams{
SaleBlock: blockHeight,
SaleTxIndex: txIndex,
OwnerPublicKey: ownerPublicKey,
DelegatedTo: delegateePublicKey,
})
if err != nil {
return errors.Wrap(err, "Can't get nodes from db")
}
nodes, err := h.nodeSaleDg.GetNodesByPubkey(ctx.UserContext(), datagateway.GetNodesByPubkeyParams{
SaleBlock: blockHeight,
SaleTxIndex: txIndex,
OwnerPublicKey: ownerPublicKey,
DelegatedTo: delegateePublicKey,
})
if err != nil {
return errors.Wrap(err, "Can't get nodes from db")
}
responses := make([]nodeResponse, len(nodes))
for i, node := range nodes {
responses[i].DeployId = request.DeployId

View File

@@ -48,10 +48,4 @@ LEFT JOIN
sale_tx_index= $2)
AS nodes ON tiers.tier_index = nodes.tier_index
GROUP BY tiers.tier_index
ORDER BY tiers.tier_index;
-- name: GetNodesByDeployment :many
SELECT *
FROM nodes
WHERE sale_block = $1 AND
sale_tx_index = $2;
ORDER BY tiers.tier_index;

View File

@@ -666,66 +666,6 @@ func (_c *NodeSaleDataGatewayWithTx_GetNodeSale_Call) RunAndReturn(run func(cont
return _c
}
// GetNodesByDeployment provides a mock function with given fields: ctx, saleBlock, saleTxIndex
func (_m *NodeSaleDataGatewayWithTx) GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error) {
ret := _m.Called(ctx, saleBlock, saleTxIndex)
if len(ret) == 0 {
panic("no return value specified for GetNodesByDeployment")
}
var r0 []entity.Node
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int32) ([]entity.Node, error)); ok {
return rf(ctx, saleBlock, saleTxIndex)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, int32) []entity.Node); ok {
r0 = rf(ctx, saleBlock, saleTxIndex)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]entity.Node)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, int32) error); ok {
r1 = rf(ctx, saleBlock, saleTxIndex)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNodesByDeployment'
type NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call struct {
*mock.Call
}
// GetNodesByDeployment is a helper method to define mock.On call
// - ctx context.Context
// - saleBlock int64
// - saleTxIndex int32
func (_e *NodeSaleDataGatewayWithTx_Expecter) GetNodesByDeployment(ctx interface{}, saleBlock interface{}, saleTxIndex interface{}) *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call {
return &NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call{Call: _e.mock.On("GetNodesByDeployment", ctx, saleBlock, saleTxIndex)}
}
func (_c *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call) Run(run func(ctx context.Context, saleBlock int64, saleTxIndex int32)) *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int64), args[2].(int32))
})
return _c
}
func (_c *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call) Return(_a0 []entity.Node, _a1 error) *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call) RunAndReturn(run func(context.Context, int64, int32) ([]entity.Node, error)) *NodeSaleDataGatewayWithTx_GetNodesByDeployment_Call {
_c.Call.Return(run)
return _c
}
// GetNodesByIds provides a mock function with given fields: ctx, arg
func (_m *NodeSaleDataGatewayWithTx) GetNodesByIds(ctx context.Context, arg datagateway.GetNodesByIdsParams) ([]entity.Node, error) {
ret := _m.Called(ctx, arg)

View File

@@ -23,7 +23,6 @@ type NodeSaleDataGateway interface {
CreateNode(ctx context.Context, arg entity.Node) error
GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error)
GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]entity.Node, error)
GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error)
GetEventsByWallet(ctx context.Context, walletAddress string) ([]entity.NodeSaleEvent, error)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.26.0
// source: nodes.sql
package gen
@@ -103,47 +103,6 @@ func (q *Queries) GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountB
return items, nil
}
const getNodesByDeployment = `-- name: GetNodesByDeployment :many
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
FROM nodes
WHERE sale_block = $1 AND
sale_tx_index = $2
`
type GetNodesByDeploymentParams struct {
SaleBlock int64
SaleTxIndex int32
}
func (q *Queries) GetNodesByDeployment(ctx context.Context, arg GetNodesByDeploymentParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodesByDeployment, arg.SaleBlock, arg.SaleTxIndex)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Node
for rows.Next() {
var i Node
if err := rows.Scan(
&i.SaleBlock,
&i.SaleTxIndex,
&i.NodeID,
&i.TierIndex,
&i.DelegatedTo,
&i.OwnerPublicKey,
&i.PurchaseTxHash,
&i.DelegateTxHash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNodesByIds = `-- name: GetNodesByIds :many
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
FROM nodes

View File

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

View File

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

View File

@@ -234,14 +234,3 @@ func (repo *Repository) GetEventsByWallet(ctx context.Context, walletAddress str
}
return mapNodeSalesEvents(events), nil
}
func (repo *Repository) GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error) {
nodes, err := repo.queries.GetNodesByDeployment(ctx, gen.GetNodesByDeploymentParams{
SaleBlock: saleBlock,
SaleTxIndex: saleTxIndex,
})
if err != nil {
return nil, errors.Wrap(err, "cannot get nodes by deploy")
}
return mapNodes(nodes), nil
}

View File

@@ -1,49 +1,29 @@
package httphandler
import (
"net/url"
"slices"
"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"
"github.com/samber/lo"
)
type getBalancesRequest struct {
paginationRequest
type getBalancesByAddressRequest struct {
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
const (
getBalancesMaxLimit = 5000
getBalancesDefaultLimit = 100
)
func (r *getBalancesRequest) Validate() error {
func (r getBalancesByAddressRequest) Validate() error {
var errList []error
if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required"))
}
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"))
}
if r.Limit > getBalancesMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getBalancesMaxLimit))
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -56,15 +36,15 @@ type balance struct {
Decimals uint8 `json:"decimals"`
}
type getBalancesResult struct {
type getBalancesByAddressResult struct {
List []balance `json:"list"`
BlockHeight uint64 `json:"blockHeight"`
}
type getBalancesResponse = HttpResponse[getBalancesResult]
type getBalancesByAddressResponse = HttpResponse[getBalancesByAddressResult]
func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
var req getBalancesRequest
func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
@@ -74,9 +54,6 @@ func (h *HttpHandler) GetBalances(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 {
@@ -87,52 +64,49 @@ func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
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)
}
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight, req.Limit, req.Offset)
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("balances not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id)
if ok {
// filter out balances that don't match the requested rune id
balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
return b.RuneId == runeId
})
for key := range balances {
if key != runeId {
delete(balances, key)
}
}
}
balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
return b.RuneId
})
balanceRuneIds := lo.Keys(balances)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), balanceRuneIds)
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
balanceList := make([]balance, 0, len(balances))
for _, b := range balances {
runeEntry := runeEntries[b.RuneId]
for id, b := range balances {
runeEntry := runeEntries[id]
balanceList = append(balanceList, balance{
Amount: b.Amount,
Id: b.RuneId,
Id: id,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
Decimals: runeEntry.Divisibility,
})
}
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
resp := getBalancesResponse{
Result: &getBalancesResult{
resp := getBalancesByAddressResponse{
Result: &getBalancesByAddressResult{
BlockHeight: blockHeight,
List: balanceList,
},

View File

@@ -3,11 +3,10 @@ package httphandler
import (
"context"
"fmt"
"slices"
"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/gofiber/fiber/v2"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
@@ -17,49 +16,33 @@ type getBalanceQuery struct {
Wallet string `json:"wallet"`
Id string `json:"id"`
BlockHeight uint64 `json:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type getBalancesBatchRequest struct {
type getBalancesByAddressBatchRequest struct {
Queries []getBalanceQuery `json:"queries"`
}
const getBalancesBatchMaxQueries = 100
func (r getBalancesBatchRequest) Validate() error {
func (r getBalancesByAddressBatchRequest) Validate() error {
var errList []error
if len(r.Queries) == 0 {
errList = append(errList, errors.New("at least one query is required"))
}
if len(r.Queries) > getBalancesBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot exceed %d queries", getBalancesBatchMaxQueries))
}
for i, query := range r.Queries {
for _, query := range r.Queries {
if query.Wallet == "" {
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required", i))
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required"))
}
if query.Id != "" && !isRuneIdOrRuneName(query.Id) {
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))
}
if query.Limit > getBalancesMaxLimit {
errList = append(errList, errors.Errorf("queries[%d]: 'limit' cannot exceed %d", i, getBalancesMaxLimit))
errList = append(errList, errors.Errorf("queries[%d]: 'id' is not valid rune id or rune name"))
}
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getBalancesBatchResult struct {
List []*getBalancesResult `json:"list"`
type getBalancesByAddressBatchResult struct {
List []*getBalancesByAddressResult `json:"list"`
}
type getBalancesBatchResponse = HttpResponse[getBalancesBatchResult]
type getBalancesByAddressBatchResponse = HttpResponse[getBalancesByAddressBatchResult]
func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
var req getBalancesBatchRequest
func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
var req getBalancesByAddressBatchRequest
if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err)
}
@@ -70,14 +53,11 @@ func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
var latestBlockHeight uint64
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")
}
latestBlockHeight = uint64(blockHeader.Height)
processQuery := func(ctx context.Context, query getBalanceQuery, queryIndex int) (*getBalancesResult, error) {
processQuery := func(ctx context.Context, query getBalanceQuery, queryIndex int) (*getBalancesByAddressResult, error) {
pkScript, ok := resolvePkScript(h.network, query.Wallet)
if !ok {
return nil, errs.NewPublicError(fmt.Sprintf("unable to resolve pkscript from \"queries[%d].wallet\"", queryIndex))
@@ -88,57 +68,50 @@ func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
blockHeight = latestBlockHeight
}
if query.Limit == 0 {
query.Limit = getBalancesDefaultLimit
}
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight, query.Limit, query.Offset)
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return nil, errs.NewPublicError("balances not found")
}
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
runeId, ok := h.resolveRuneId(ctx, query.Id)
if ok {
// filter out balances that don't match the requested rune id
balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
return b.RuneId == runeId
})
for key := range balances {
if key != runeId {
delete(balances, key)
}
}
}
balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
return b.RuneId
})
balanceRuneIds := lo.Keys(balances)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx, balanceRuneIds)
if err != nil {
if errors.Is(err, errs.NotFound) {
return nil, errs.NewPublicError("rune not found")
}
return nil, errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
balanceList := make([]balance, 0, len(balances))
for _, b := range balances {
runeEntry := runeEntries[b.RuneId]
for id, b := range balances {
runeEntry := runeEntries[id]
balanceList = append(balanceList, balance{
Amount: b.Amount,
Id: b.RuneId,
Id: id,
Name: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
Decimals: runeEntry.Divisibility,
})
}
slices.SortFunc(balanceList, func(i, j balance) int {
return j.Amount.Cmp(i.Amount)
})
result := getBalancesResult{
result := getBalancesByAddressResult{
BlockHeight: blockHeight,
List: balanceList,
}
return &result, nil
}
results := make([]*getBalancesResult, len(req.Queries))
results := make([]*getBalancesByAddressResult, len(req.Queries))
eg, ectx := errgroup.WithContext(ctx.UserContext())
for i, query := range req.Queries {
i := i
@@ -156,8 +129,8 @@ func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
return errors.WithStack(err)
}
resp := getBalancesBatchResponse{
Result: &getBalancesBatchResult{
resp := getBalancesByAddressBatchResponse{
Result: &getBalancesByAddressBatchResult{
List: results,
},
}

View File

@@ -1,12 +1,28 @@
package httphandler
import (
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/constants"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gofiber/fiber/v2"
)
var startingBlockHeader = map[common.Network]types.BlockHeader{
common.NetworkMainnet: {
Height: 839999,
Hash: *utils.Must(chainhash.NewHashFromStr("0000000000000000000172014ba58d66455762add0512355ad651207918494ab")),
PrevBlock: *utils.Must(chainhash.NewHashFromStr("00000000000000000001dcce6ce7c8a45872cafd1fb04732b447a14a91832591")),
},
common.NetworkTestnet: {
Height: 2583200,
Hash: *utils.Must(chainhash.NewHashFromStr("000000000006c5f0dfcd9e0e81f27f97a87aef82087ffe69cd3c390325bb6541")),
PrevBlock: *utils.Must(chainhash.NewHashFromStr("00000000000668f3bafac992f53424774515440cb47e1cb9e73af3f496139e28")),
},
}
type getCurrentBlockResult struct {
Hash string `json:"hash"`
Height int64 `json:"height"`
@@ -20,7 +36,7 @@ func (h *HttpHandler) GetCurrentBlock(ctx *fiber.Ctx) (err error) {
if !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeader = constants.StartingBlockHeader[h.network]
blockHeader = startingBlockHeader[h.network]
}
resp := getCurrentBlockResponse{

View File

@@ -1,15 +1,10 @@
package httphandler
import (
"bytes"
"encoding/hex"
"fmt"
"net/url"
"slices"
"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"
@@ -17,30 +12,14 @@ import (
)
type getHoldersRequest struct {
paginationRequest
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
const (
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.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"))
}
if r.Limit > getHoldersMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getHoldersMaxLimit))
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -56,7 +35,6 @@ type getHoldersResult struct {
BlockHeight uint64 `json:"blockHeight"`
TotalSupply uint128.Uint128 `json:"totalSupply"`
MintedAmount uint128.Uint128 `json:"mintedAmount"`
Decimals uint8 `json:"decimals"`
List []holdingBalance `json:"list"`
}
@@ -73,9 +51,6 @@ 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 {
@@ -91,7 +66,7 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
var ok bool
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
if !ok {
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
return errs.NewPublicError("unable to resolve rune id from \"id\"")
}
}
@@ -100,13 +75,10 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
return errors.Wrap(err, "error during GetHoldersByHeight")
}
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, req.Limit, req.Offset)
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("balances not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
@@ -132,20 +104,11 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
})
}
// sort by amount descending, then pk script ascending
slices.SortFunc(holdingBalances, func(b1, b2 *entity.Balance) int {
if b1.Amount.Cmp(b2.Amount) == 0 {
return bytes.Compare(b1.PkScript, b2.PkScript)
}
return b2.Amount.Cmp(b1.Amount)
})
resp := getHoldersResponse{
Result: &getHoldersResult{
BlockHeight: blockHeight,
TotalSupply: totalSupply,
MintedAmount: mintedAmount,
Decimals: runeEntry.Divisibility,
List: list,
},
}

View File

@@ -1,12 +1,11 @@
package httphandler
import (
"fmt"
"net/url"
"strings"
"slices"
"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"
@@ -14,29 +13,15 @@ import (
)
type getTokenInfoRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
AdditionalFields []string
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r *getTokenInfoRequest) Validate() error {
func (r getTokenInfoRequest) Validate() error {
var errList []error
id, err := url.QueryUnescape(r.Id)
if err != nil {
return errors.WithStack(err)
}
r.Id = id
if !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
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")
}
@@ -50,19 +35,17 @@ 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"`
EtchingTxHash string `json:"etchingTxHash"`
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"`
}
type tokenInfoExtend struct {
HoldersCount *int64 `json:"holdersCount,omitempty"`
Entry entry `json:"entry"`
Entry entry `json:"entry"`
}
type getTokenInfoResult struct {
@@ -74,11 +57,11 @@ type getTokenInfoResult struct {
MintedAmount uint128.Uint128 `json:"mintedAmount"`
BurnedAmount uint128.Uint128 `json:"burnedAmount"`
Decimals uint8 `json:"decimals"`
DeployedAt int64 `json:"deployedAt"` // unix timestamp
DeployedAt uint64 `json:"deployedAt"` // unix timestamp
DeployedAtHeight uint64 `json:"deployedAtHeight"`
CompletedAt *int64 `json:"completedAt"` // unix timestamp
CompletedAt *uint64 `json:"completedAt"` // unix timestamp
CompletedAtHeight *uint64 `json:"completedAtHeight"`
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
HoldersCount int `json:"holdersCount"`
Extend tokenInfoExtend `json:"extend"`
}
@@ -100,9 +83,6 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
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)
@@ -113,7 +93,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(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
return errs.NewPublicError("unable to resolve rune id from \"id\"")
}
}
@@ -122,78 +102,67 @@ 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 GetRuneEntryByRuneIdAndHeight")
return errors.Wrap(err, "error during GetTokenInfoByHeight")
}
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")
}
holdersCountPtr = &holdersCount
}
result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
return errors.Wrap(err, "error during createTokenInfoResult")
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
resp := getTokenInfoResponse{
Result: result,
}
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)
})
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")
return 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")
return 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,
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,
},
Turbo: runeEntry.Turbo,
EtchingTxHash: runeEntry.EtchingTxHash.String(),
},
},
}, nil
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -1,118 +0,0 @@
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

@@ -1,150 +0,0 @@
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

@@ -1,171 +0,0 @@
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

@@ -1,10 +1,8 @@
package httphandler
import (
"cmp"
"encoding/hex"
"fmt"
"net/url"
"slices"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -17,28 +15,17 @@ import (
)
type getTransactionsRequest struct {
paginationRequest
Wallet string `query:"wallet"`
Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
ToBlock int64 `query:"toBlock"`
Wallet string `query:"wallet"`
Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
ToBlock int64 `query:"toBlock"`
}
const (
getTransactionsMaxLimit = 3000
)
func (r *getTransactionsRequest) Validate() error {
func (r getTransactionsRequest) Validate() error {
var errList []error
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.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
if r.FromBlock < -1 {
errList = append(errList, errors.Errorf("invalid fromBlock range"))
@@ -46,12 +33,6 @@ func (r *getTransactionsRequest) Validate() error {
if r.ToBlock < -1 {
errList = append(errList, errors.Errorf("invalid toBlock range"))
}
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getTransactionsMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getTransactionsMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -134,9 +115,6 @@ 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 != "" {
@@ -152,7 +130,7 @@ 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(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
return errs.NewPublicError("unable to resolve rune id from \"id\"")
}
}
@@ -165,9 +143,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
if req.FromBlock == -1 || req.ToBlock == -1 {
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")
}
if req.FromBlock == -1 {
@@ -183,34 +158,29 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError(fmt.Sprintf("fromBlock must be less than or equal to toBlock, got fromBlock=%d, toBlock=%d", req.FromBlock, req.ToBlock))
}
txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, uint64(req.FromBlock), uint64(req.ToBlock), req.Limit, req.Offset)
txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, uint64(req.FromBlock), uint64(req.ToBlock))
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("transactions not found")
}
return errors.Wrap(err, "error during GetRuneTransactions")
}
allRuneIds := make(map[runes.RuneId]struct{})
var allRuneIds []runes.RuneId
for _, tx := range txs {
for id := range tx.Mints {
allRuneIds[id] = struct{}{}
allRuneIds = append(allRuneIds, id)
}
for id := range tx.Burns {
allRuneIds[id] = struct{}{}
allRuneIds = append(allRuneIds, id)
}
for _, input := range tx.Inputs {
allRuneIds[input.RuneId] = struct{}{}
allRuneIds = append(allRuneIds, input.RuneId)
}
for _, output := range tx.Outputs {
allRuneIds[output.RuneId] = struct{}{}
allRuneIds = append(allRuneIds, output.RuneId)
}
}
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
allRuneIds = lo.Uniq(allRuneIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
@@ -309,12 +279,12 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
}
txList = append(txList, respTx)
}
// sort by block height DESC, then index DESC
// sort by block height ASC, then index ASC
slices.SortFunc(txList, func(t1, t2 transaction) int {
if t1.BlockHeight != t2.BlockHeight {
return cmp.Compare(t2.BlockHeight, t1.BlockHeight)
return int(t1.BlockHeight - t2.BlockHeight)
}
return cmp.Compare(t2.Index, t1.Index)
return int(t1.Index - t2.Index)
})
resp := getTransactionsResponse{

View File

@@ -1,9 +1,8 @@
package httphandler
import (
"net/url"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
@@ -13,37 +12,19 @@ import (
"github.com/samber/lo"
)
type getUTXOsRequest struct {
paginationRequest
type getUTXOsByAddressRequest struct {
Wallet string `params:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
const (
getUTXOsMaxLimit = 3000
)
func (r *getUTXOsRequest) Validate() error {
func (r getUTXOsByAddressRequest) Validate() error {
var errList []error
if r.Wallet == "" {
errList = append(errList, errors.New("'wallet' is required"))
}
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"))
}
if r.Limit > getUTXOsMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getUTXOsMaxLimit))
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -60,22 +41,21 @@ type utxoExtend struct {
Runes []runeBalance `json:"runes"`
}
type utxoItem struct {
type utxo struct {
TxHash chainhash.Hash `json:"txHash"`
OutputIndex uint32 `json:"outputIndex"`
Sats int64 `json:"sats"`
Extend utxoExtend `json:"extend"`
}
type getUTXOsResult struct {
List []utxoItem `json:"list"`
BlockHeight uint64 `json:"blockHeight"`
type getUTXOsByAddressResult struct {
List []utxo `json:"list"`
BlockHeight uint64 `json:"blockHeight"`
}
type getUTXOsResponse = HttpResponse[getUTXOsResult]
type getUTXOsByAddressResponse = HttpResponse[getUTXOsByAddressResult]
func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
var req getUTXOsRequest
func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) {
var req getUTXOsByAddressRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
@@ -85,9 +65,6 @@ 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 {
@@ -98,52 +75,32 @@ func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
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)
}
var utxos []*entity.RunesUTXOWithSats
if runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id); ok {
utxos, err = h.usecase.GetRunesUTXOsByRuneIdAndPkScript(ctx.UserContext(), runeId, pkScript, blockHeight, req.Limit, req.Offset)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("utxos not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
} else {
utxos, err = h.usecase.GetRunesUTXOsByPkScript(ctx.UserContext(), pkScript, blockHeight, req.Limit, req.Offset)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("utxos not found")
}
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
outPointBalances, err := h.usecase.GetUnspentOutPointBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetBalancesByPkScript")
}
runeIds := make(map[runes.RuneId]struct{}, 0)
for _, utxo := range utxos {
for _, balance := range utxo.RuneBalances {
runeIds[balance.RuneId] = struct{}{}
}
}
runeIdsList := lo.Keys(runeIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), runeIdsList)
outPointBalanceRuneIds := lo.Map(outPointBalances, func(outPointBalance *entity.OutPointBalance, _ int) runes.RuneId {
return outPointBalance.RuneId
})
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), outPointBalanceRuneIds)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
utxoRespList := make([]utxoItem, 0, len(utxos))
for _, utxo := range utxos {
runeBalances := make([]runeBalance, 0, len(utxo.RuneBalances))
for _, balance := range utxo.RuneBalances {
groupedBalances := lo.GroupBy(outPointBalances, func(outPointBalance *entity.OutPointBalance) wire.OutPoint {
return outPointBalance.OutPoint
})
utxoList := make([]utxo, 0, len(groupedBalances))
for outPoint, balances := range groupedBalances {
runeBalances := make([]runeBalance, 0, len(balances))
for _, balance := range balances {
runeEntry := runeEntries[balance.RuneId]
runeBalances = append(runeBalances, runeBalance{
RuneId: balance.RuneId,
@@ -154,20 +111,34 @@ func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
})
}
utxoRespList = append(utxoRespList, utxoItem{
TxHash: utxo.OutPoint.Hash,
OutputIndex: utxo.OutPoint.Index,
Sats: utxo.Sats,
utxoList = append(utxoList, utxo{
TxHash: outPoint.Hash,
OutputIndex: outPoint.Index,
Extend: utxoExtend{
Runes: runeBalances,
},
})
}
resp := getUTXOsResponse{
Result: &getUTXOsResult{
// filter by req.Id if exists
{
runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id)
if ok {
utxoList = lo.Filter(utxoList, func(u utxo, _ int) bool {
for _, runeBalance := range u.Extend.Runes {
if runeBalance.RuneId == runeId {
return true
}
}
return false
})
}
}
resp := getUTXOsByAddressResponse{
Result: &getUTXOsByAddressResult{
BlockHeight: blockHeight,
List: utxoRespList,
List: utxoList,
},
}

View File

@@ -1,92 +0,0 @@
package httphandler
import (
"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/gaze-network/indexer-network/modules/runes/usecase"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
type getUTXOsOutputByLocationRequest struct {
TxHash string `params:"txHash"`
OutputIndex int32 `query:"outputIndex"`
}
func (r getUTXOsOutputByLocationRequest) Validate() error {
var errList []error
if r.TxHash == "" {
errList = append(errList, errors.New("'txHash' is required"))
}
if r.OutputIndex < 0 {
errList = append(errList, errors.New("'outputIndex' must be non-negative"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getUTXOsOutputByTxIdResponse = HttpResponse[utxoItem]
func (h *HttpHandler) GetUTXOsOutputByLocation(ctx *fiber.Ctx) (err error) {
var req getUTXOsOutputByLocationRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := ctx.QueryParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
txHash, err := chainhash.NewHashFromStr(req.TxHash)
if err != nil {
return errs.WithPublicMessage(err, "unable to resolve txHash")
}
utxo, err := h.usecase.GetUTXOsOutputByLocation(ctx.UserContext(), *txHash, uint32(req.OutputIndex))
if err != nil {
if errors.Is(err, usecase.ErrUTXONotFound) {
return errs.NewPublicError("utxo not found")
}
return errors.WithStack(err)
}
runeIds := make(map[runes.RuneId]struct{}, 0)
for _, balance := range utxo.RuneBalances {
runeIds[balance.RuneId] = struct{}{}
}
runeIdsList := lo.Keys(runeIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), runeIdsList)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
runeBalances := make([]runeBalance, 0, len(utxo.RuneBalances))
for _, balance := range utxo.RuneBalances {
runeEntry := runeEntries[balance.RuneId]
runeBalances = append(runeBalances, runeBalance{
RuneId: balance.RuneId,
Rune: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
Amount: balance.Amount,
Divisibility: runeEntry.Divisibility,
})
}
resp := getUTXOsOutputByTxIdResponse{
Result: &utxoItem{
TxHash: utxo.OutPoint.Hash,
OutputIndex: utxo.OutPoint.Index,
Sats: utxo.Sats,
Extend: utxoExtend{
Runes: runeBalances,
},
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -1,136 +0,0 @@
package httphandler
import (
"context"
"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/gaze-network/indexer-network/modules/runes/usecase"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
type getUTXOsOutputByLocationQuery struct {
TxHash string `json:"txHash"`
OutputIndex int32 `json:"outputIndex"`
}
type getUTXOsOutputByLocationBatchRequest struct {
Queries []getUTXOsOutputByLocationQuery `json:"queries"`
}
const getUTXOsOutputByLocationBatchMaxQueries = 100
func (r getUTXOsOutputByLocationBatchRequest) Validate() error {
var errList []error
if len(r.Queries) == 0 {
errList = append(errList, errors.New("at least one query is required"))
}
if len(r.Queries) > getUTXOsOutputByLocationBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot exceed %d queries", getUTXOsOutputByLocationBatchMaxQueries))
}
for i, query := range r.Queries {
if query.TxHash == "" {
errList = append(errList, errors.Errorf("queries[%d]: 'txHash' is required", i))
}
if query.OutputIndex < 0 {
errList = append(errList, errors.Errorf("queries[%d]: 'outputIndex' must be non-negative", i))
}
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getUTXOsOutputByLocationBatchResult struct {
List []*utxoItem `json:"list"`
}
type getUTXOsOutputByLocationBatchResponse = HttpResponse[getUTXOsOutputByLocationBatchResult]
func (h *HttpHandler) GetUTXOsOutputByLocationBatch(ctx *fiber.Ctx) (err error) {
var req getUTXOsOutputByLocationBatchRequest
if err := ctx.BodyParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
processQuery := func(ctx context.Context, query getUTXOsOutputByLocationQuery, queryIndex int) (*utxoItem, error) {
txHash, err := chainhash.NewHashFromStr(query.TxHash)
if err != nil {
return nil, errs.WithPublicMessage(err, fmt.Sprintf("unable to parse txHash from \"queries[%d].txHash\"", queryIndex))
}
utxo, err := h.usecase.GetUTXOsOutputByLocation(ctx, *txHash, uint32(query.OutputIndex))
if err != nil {
if errors.Is(err, usecase.ErrUTXONotFound) {
return nil, errs.NewPublicError(fmt.Sprintf("utxo not found for queries[%d]", queryIndex))
}
return nil, errors.WithStack(err)
}
runeIds := make(map[runes.RuneId]struct{}, 0)
for _, balance := range utxo.RuneBalances {
runeIds[balance.RuneId] = struct{}{}
}
runeIdsList := lo.Keys(runeIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx, runeIdsList)
if err != nil {
if errors.Is(err, errs.NotFound) {
return nil, errs.NewPublicError(fmt.Sprintf("rune entries not found for queries[%d]", queryIndex))
}
return nil, errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
runeBalances := make([]runeBalance, 0, len(utxo.RuneBalances))
for _, balance := range utxo.RuneBalances {
runeEntry := runeEntries[balance.RuneId]
runeBalances = append(runeBalances, runeBalance{
RuneId: balance.RuneId,
Rune: runeEntry.SpacedRune,
Symbol: string(runeEntry.Symbol),
Amount: balance.Amount,
Divisibility: runeEntry.Divisibility,
})
}
return &utxoItem{
TxHash: utxo.OutPoint.Hash,
OutputIndex: utxo.OutPoint.Index,
Sats: utxo.Sats,
Extend: utxoExtend{
Runes: runeBalances,
},
}, nil
}
results := make([]*utxoItem, len(req.Queries))
eg, ectx := errgroup.WithContext(ctx.UserContext())
for i, query := range req.Queries {
i := i
query := query
eg.Go(func() error {
result, err := processQuery(ectx, query, i)
if err != nil {
return errors.Wrapf(err, "error during processQuery for query %d", i)
}
results[i] = result
return nil
})
}
if err := eg.Wait(); err != nil {
return errors.WithStack(err)
}
resp := getUTXOsOutputByLocationBatchResponse{
Result: &getUTXOsOutputByLocationBatchResult{
List: results,
},
}
return errors.WithStack(ctx.JSON(resp))
}

View File

@@ -7,9 +7,7 @@ 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"
@@ -33,53 +31,6 @@ 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
@@ -90,10 +41,6 @@ func resolvePkScript(network common.Network, wallet string) ([]byte, bool) {
return &chaincfg.MainNetParams
case common.NetworkTestnet:
return &chaincfg.TestNet3Params
case common.NetworkFractalMainnet:
return &chaincfg.MainNetParams
case common.NetworkFractalTestnet:
return &chaincfg.MainNetParams
}
panic("invalid network")
}()

View File

@@ -7,17 +7,12 @@ import (
func (h *HttpHandler) Mount(router fiber.Router) error {
r := router.Group("/v2/runes")
r.Post("/balances/wallet/batch", h.GetBalancesBatch)
r.Get("/balances/wallet/:wallet", h.GetBalances)
r.Post("/balances/wallet/batch", h.GetBalancesByAddressBatch)
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress)
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("/utxos/wallet/:wallet", h.GetUTXOsByAddress)
r.Get("/block", h.GetCurrentBlock)
r.Get("/tokens", h.GetTokens)
return nil
}

View File

@@ -0,0 +1,27 @@
package runes
import (
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/core/types"
)
const (
Version = "v0.0.1"
DBVersion = 1
EventHashVersion = 1
)
var startingBlockHeader = map[common.Network]types.BlockHeader{
common.NetworkMainnet: {
Height: 839999,
Hash: *utils.Must(chainhash.NewHashFromStr("0000000000000000000172014ba58d66455762add0512355ad651207918494ab")),
PrevBlock: *utils.Must(chainhash.NewHashFromStr("00000000000000000001dcce6ce7c8a45872cafd1fb04732b447a14a91832591")),
},
common.NetworkTestnet: {
Height: 2583200,
Hash: *utils.Must(chainhash.NewHashFromStr("000000000006c5f0dfcd9e0e81f27f97a87aef82087ffe69cd3c390325bb6541")),
PrevBlock: *utils.Must(chainhash.NewHashFromStr("00000000000668f3bafac992f53424774515440cb47e1cb9e73af3f496139e28")),
},
}

View File

@@ -1,122 +0,0 @@
package constants
import (
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/uint128"
"github.com/samber/lo"
)
const (
Version = "v0.0.1"
DBVersion = 1
EventHashVersion = 1
)
// starting block heights and hashes should be 1 block before activation block, as indexer will start from the block after this value
var StartingBlockHeader = map[common.Network]types.BlockHeader{
common.NetworkMainnet: {
Height: 839999,
},
common.NetworkTestnet: {
Height: 2519999,
},
common.NetworkFractalMainnet: {
Height: 83999,
},
common.NetworkFractalTestnet: {
Height: 83999,
},
}
type GenesisRuneConfig struct {
RuneId runes.RuneId
Name string
Number uint64
Divisibility uint8
Premine uint128.Uint128
SpacedRune runes.SpacedRune
Symbol rune
Terms *runes.Terms
Turbo bool
EtchingTxHash chainhash.Hash
EtchedAt time.Time
}
var GenesisRuneConfigMap = map[common.Network]GenesisRuneConfig{
common.NetworkMainnet: {
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
Number: 0,
Divisibility: 0,
Premine: uint128.Zero,
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
Symbol: '\u29c9',
Terms: &runes.Terms{
Amount: lo.ToPtr(uint128.From64(1)),
Cap: &uint128.Max,
HeightStart: lo.ToPtr(uint64(840000)),
HeightEnd: lo.ToPtr(uint64(1050000)),
OffsetStart: nil,
OffsetEnd: nil,
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Unix(0, 0),
},
common.NetworkFractalMainnet: {
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
Number: 0,
Divisibility: 0,
Premine: uint128.Zero,
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
Symbol: '\u29c9',
Terms: &runes.Terms{
Amount: lo.ToPtr(uint128.From64(1)),
Cap: &uint128.Max,
HeightStart: lo.ToPtr(uint64(84000)),
HeightEnd: lo.ToPtr(uint64(2184000)),
OffsetStart: nil,
OffsetEnd: nil,
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Unix(0, 0),
},
common.NetworkFractalTestnet: {
RuneId: runes.RuneId{BlockHeight: 1, TxIndex: 0},
Number: 0,
Divisibility: 0,
Premine: uint128.Zero,
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
Symbol: '\u29c9',
Terms: &runes.Terms{
Amount: lo.ToPtr(uint128.From64(1)),
Cap: &uint128.Max,
HeightStart: lo.ToPtr(uint64(84000)),
HeightEnd: lo.ToPtr(uint64(2184000)),
OffsetStart: nil,
OffsetEnd: nil,
},
Turbo: true,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Unix(0, 0),
},
}
func NetworkHasGenesisRune(network common.Network) bool {
switch network {
case common.NetworkMainnet, common.NetworkFractalMainnet, common.NetworkFractalTestnet:
return true
case common.NetworkTestnet:
return false
default:
logger.Panic(fmt.Sprintf("unsupported network: %s", network))
return false
}
}

View File

@@ -1,6 +1,5 @@
BEGIN;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Indexer Client Information
CREATE TABLE IF NOT EXISTS "runes_indexer_stats" (
@@ -49,7 +48,6 @@ 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" (
@@ -120,7 +118,5 @@ CREATE TABLE IF NOT EXISTS "runes_balances" (
"amount" DECIMAL NOT NULL,
PRIMARY KEY ("pkscript", "rune_id", "block_height")
);
CREATE INDEX IF NOT EXISTS runes_balances_rune_id_block_height_idx ON "runes_balances" USING BTREE ("rune_id", "block_height");
CREATE INDEX IF NOT EXISTS runes_balances_pkscript_block_height_idx ON "runes_balances" USING BTREE ("pkscript", "block_height");
COMMIT;

View File

@@ -1,104 +0,0 @@
-- 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

@@ -2,48 +2,22 @@
WITH balances AS (
SELECT DISTINCT ON (rune_id) * FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
)
SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4;
SELECT * FROM balances WHERE amount > 0;
-- name: GetBalancesByRuneId :many
WITH balances AS (
SELECT DISTINCT ON (pkscript) * FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
)
SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4;
SELECT * FROM balances WHERE amount > 0;
-- 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;
-- name: GetRunesUTXOsByPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> @rune_ids::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetUnspentOutPointBalancesByPkScript :many
SELECT * FROM runes_outpoint_balances WHERE pkscript = @pkScript AND block_height <= @block_height AND (spent_height IS NULL OR spent_height > @block_height);
-- name: GetRuneEntriesByRuneIds :many
WITH states AS (
@@ -63,49 +37,6 @@ 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;
@@ -126,12 +57,7 @@ SELECT * FROM runes_transactions
) AND (
@from_block <= runes_transactions.block_height AND runes_transactions.block_height <= @to_block
)
ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2;
-- name: GetRuneTransaction :one
SELECT * FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE hash = $1 LIMIT 1;
ORDER BY runes_transactions.block_height DESC LIMIT 10000;
-- name: CountRuneEntries :one
SELECT COUNT(*) FROM runes_entries;
@@ -150,13 +76,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: CreateOutPointBalance :exec
-- name: CreateOutPointBalances :batchexec
INSERT INTO runes_outpoint_balances (rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height) VALUES ($1, $2, $3, $4, $5, $6, $7);
-- name: SpendOutPointBalance :exec
-- name: SpendOutPointBalances :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3;
-- name: CreateRuneBalance :exec
-- name: CreateRuneBalanceAtBlock :batchexec
INSERT INTO runes_balances (pkscript, block_height, rune_id, amount) VALUES ($1, $2, $3, $4);
-- name: GetLatestIndexedBlock :one

View File

@@ -3,11 +3,11 @@ package datagateway
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"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 {
@@ -27,12 +27,10 @@ type RunesReaderDataGateway interface {
GetLatestBlock(ctx context.Context) (types.BlockHeader, error)
GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error)
// GetRuneTransactions returns the runes transactions, filterable by pkScript, runeId and height. If pkScript, runeId or height is zero value, that filter is ignored.
GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error)
GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error)
GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error)
GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error)
GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error)
// GetRuneIdFromRune returns the RuneId for the given rune. Returns errs.NotFound if the rune entry is not found.
GetRuneIdFromRune(ctx context.Context, rune runes.Rune) (runes.RuneId, error)
// GetRuneEntryByRuneId returns the RuneEntry for the given runeId. Returns errs.NotFound if the rune entry is not found.
@@ -43,33 +41,25 @@ 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)
// GetBalancesByPkScript returns the balances for the given pkScript at the given blockHeight.
// Use limit = -1 as no limit.
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error)
// GetBalancesByRuneId returns the balances for the given runeId at the given blockHeight.
// Cannot use []byte as map key, so we're returning as slice.
// Use limit = -1 as no limit.
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*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 {
CreateRuneEntries(ctx context.Context, entries []*runes.RuneEntry) error
CreateRuneEntryStates(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error
CreateRuneEntry(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error
CreateRuneEntryState(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error
CreateOutPointBalances(ctx context.Context, outPointBalances []*entity.OutPointBalance) 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
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
CreateIndexedBlock(ctx context.Context, block *entity.IndexedBlock) error
// TODO: collapse these into a single function (ResetStateToHeight)?
@@ -82,3 +72,10 @@ 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

@@ -11,7 +11,6 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"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/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
@@ -29,7 +28,7 @@ func (p *Processor) calculateEventHash(header types.BlockHeader) (chainhash.Hash
func (p *Processor) getHashPayload(header types.BlockHeader) ([]byte, error) {
var sb strings.Builder
sb.WriteString("payload:v" + strconv.Itoa(constants.EventHashVersion) + ":")
sb.WriteString("payload:v" + strconv.Itoa(EventHashVersion) + ":")
sb.WriteString("blockHash:")
sb.Write(header.Hash[:])

View File

@@ -1,23 +0,0 @@
package entity
import (
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
)
type RunesUTXOBalance struct {
RuneId runes.RuneId
Amount uint128.Uint128
}
type RunesUTXO struct {
PkScript []byte
OutPoint wire.OutPoint
RuneBalances []RunesUTXOBalance
}
type RunesUTXOWithSats struct {
RunesUTXO
Sats int64
}

View File

@@ -4,13 +4,13 @@ import (
"context"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexer"
"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"
@@ -19,6 +19,7 @@ import (
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gaze-network/uint128"
"github.com/samber/lo"
)
// Make sure to implement the Bitcoin Processor interface
@@ -67,8 +68,8 @@ func (p *Processor) VerifyStates(ctx context.Context) error {
if err := p.ensureValidState(ctx); err != nil {
return errors.Wrap(err, "error during ensureValidState")
}
if constants.NetworkHasGenesisRune(p.network) {
if err := p.ensureGenesisRune(ctx, p.network); err != nil {
if p.network == common.NetworkMainnet {
if err := p.ensureGenesisRune(ctx); err != nil {
return errors.Wrap(err, "error during ensureGenesisRune")
}
}
@@ -88,17 +89,17 @@ func (p *Processor) ensureValidState(ctx context.Context) error {
// if not found, set indexer state
if errors.Is(err, errs.NotFound) {
if err := p.indexerInfoDg.SetIndexerState(ctx, entity.IndexerState{
DBVersion: constants.DBVersion,
EventHashVersion: constants.EventHashVersion,
DBVersion: DBVersion,
EventHashVersion: EventHashVersion,
}); err != nil {
return errors.Wrap(err, "failed to set indexer state")
}
} else {
if indexerState.DBVersion != constants.DBVersion {
return errors.Wrapf(errs.ConflictSetting, "db version mismatch: current version is %d. Please upgrade to version %d", indexerState.DBVersion, constants.DBVersion)
if indexerState.DBVersion != DBVersion {
return errors.Wrapf(errs.ConflictSetting, "db version mismatch: current version is %d. Please upgrade to version %d", indexerState.DBVersion, DBVersion)
}
if indexerState.EventHashVersion != constants.EventHashVersion {
return errors.Wrapf(errs.ConflictSetting, "event version mismatch: current version is %d. Please reset rune's db first.", indexerState.EventHashVersion, constants.EventHashVersion)
if indexerState.EventHashVersion != EventHashVersion {
return errors.Wrapf(errs.ConflictSetting, "event version mismatch: current version is %d. Please reset rune's db first.", indexerState.EventHashVersion, EventHashVersion)
}
}
@@ -118,37 +119,39 @@ func (p *Processor) ensureValidState(ctx context.Context) error {
return nil
}
func (p *Processor) ensureGenesisRune(ctx context.Context, network common.Network) error {
genesisRuneConfig, ok := constants.GenesisRuneConfigMap[network]
if !ok {
logger.Panic("genesis rune config not found", slogx.Stringer("network", network))
}
_, err := p.runesDg.GetRuneEntryByRuneId(ctx, genesisRuneConfig.RuneId)
var genesisRuneId = runes.RuneId{BlockHeight: 1, TxIndex: 0}
func (p *Processor) ensureGenesisRune(ctx context.Context) error {
_, err := p.runesDg.GetRuneEntryByRuneId(ctx, genesisRuneId)
if err != nil && !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "failed to get genesis rune entry")
}
if errors.Is(err, errs.NotFound) {
runeEntry := &runes.RuneEntry{
RuneId: genesisRuneConfig.RuneId,
Number: genesisRuneConfig.Number,
Divisibility: genesisRuneConfig.Divisibility,
Premine: genesisRuneConfig.Premine,
SpacedRune: genesisRuneConfig.SpacedRune,
Symbol: genesisRuneConfig.Symbol,
Terms: genesisRuneConfig.Terms,
Turbo: genesisRuneConfig.Turbo,
RuneId: genesisRuneId,
Number: 0,
Divisibility: 0,
Premine: uint128.Zero,
SpacedRune: runes.NewSpacedRune(runes.NewRune(2055900680524219742), 0b10000000),
Symbol: '\u29c9',
Terms: &runes.Terms{
Amount: lo.ToPtr(uint128.From64(1)),
Cap: &uint128.Max,
HeightStart: lo.ToPtr(uint64(common.HalvingInterval * 4)),
HeightEnd: lo.ToPtr(uint64(common.HalvingInterval * 5)),
OffsetStart: nil,
OffsetEnd: nil,
},
Turbo: true,
Mints: uint128.Zero,
BurnedAmount: uint128.Zero,
CompletedAt: time.Time{},
CompletedAtHeight: nil,
EtchingBlock: genesisRuneConfig.RuneId.BlockHeight,
EtchingTxHash: genesisRuneConfig.EtchingTxHash,
EtchedAt: genesisRuneConfig.EtchedAt,
EtchingBlock: 1,
EtchingTxHash: chainhash.Hash{},
EtchedAt: time.Time{},
}
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 {
if err := p.runesDg.CreateRuneEntry(ctx, runeEntry, genesisRuneId.BlockHeight); err != nil {
return errors.Wrap(err, "failed to create genesis rune entry")
}
}
@@ -163,7 +166,7 @@ func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error)
blockHeader, err := p.runesDg.GetLatestBlock(ctx)
if err != nil {
if errors.Is(err, errs.NotFound) {
return constants.StartingBlockHeader[p.network], nil
return startingBlockHeader[p.network], nil
}
return types.BlockHeader{}, errors.Wrap(err, "failed to get latest block")
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/cockroachdb/errors"
"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"
@@ -26,26 +26,19 @@ 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.InfoContext(ctx, "Processing new block",
slogx.String("event", "runes_processor_processing_block"),
slog.Int("txs", len(block.Transactions)),
)
logger.DebugContext(ctx, "Processing new 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
}
@@ -675,7 +668,6 @@ 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")
@@ -695,10 +687,10 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
return errors.Wrap(err, "failed to calculate event hash")
}
prevIndexedBlock, err := runesDgTx.GetIndexedBlockByHeight(ctx, blockHeader.Height-1)
if err != nil && errors.Is(err, errs.NotFound) && blockHeader.Height-1 == constants.StartingBlockHeader[p.network].Height {
if err != nil && errors.Is(err, errs.NotFound) && blockHeader.Height-1 == startingBlockHeader[p.network].Height {
prevIndexedBlock = &entity.IndexedBlock{
Height: constants.StartingBlockHeader[p.network].Height,
Hash: chainhash.Hash{},
Height: startingBlockHeader[p.network].Height,
Hash: startingBlockHeader[p.network].Hash,
EventHash: chainhash.Hash{},
CumulativeEventHash: chainhash.Hash{},
}
@@ -722,90 +714,86 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
return errors.Wrap(err, "failed to create indexed block")
}
// flush new rune entries
newRuneEntries := lo.Values(p.newRuneEntries)
if err := runesDgTx.CreateRuneEntries(ctx, newRuneEntries); err != nil {
return errors.Wrap(err, "failed to create rune entry")
{
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)
}
p.newRuneEntries = make(map[runes.RuneId]*runes.RuneEntry)
// flush new rune entry states
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")
{
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)
}
p.newRuneEntryStates = make(map[runes.RuneId]*runes.RuneEntry)
// flush new outpoint balances
newOutpointBalances := make([]*entity.OutPointBalance, 0)
for _, balances := range p.newOutPointBalances {
newOutpointBalances = append(newOutpointBalances, 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)
}
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
newSpendOutPoints := p.newSpendOutPoints
if err := runesDgTx.SpendOutPointBalancesBatch(ctx, newSpendOutPoints, uint64(blockHeader.Height)); err != nil {
return errors.Wrap(err, "failed to create spend outpoint")
}
p.newSpendOutPoints = make([]wire.OutPoint, 0)
// flush new 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")
{
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")
}
}
for runeId, balance := range balances {
newBalances = append(newBalances, &entity.Balance{
PkScript: pkScript,
RuneId: runeId,
Amount: balance,
BlockHeight: uint64(blockHeader.Height),
})
p.newSpendOutPoints = make([]wire.OutPoint, 0)
}
// flush new balances
{
params := make([]datagateway.CreateRuneBalancesParams, 0)
for pkScriptStr, balances := range p.newBalances {
pkScript, err := hex.DecodeString(pkScriptStr)
if err != nil {
return errors.Wrap(err, "failed to decode pk script")
}
for runeId, balance := range balances {
params = append(params, datagateway.CreateRuneBalancesParams{
PkScript: pkScript,
RuneId: runeId,
Balance: balance,
BlockHeight: uint64(blockHeader.Height),
})
}
}
if err := runesDgTx.CreateRuneBalances(ctx, params); err != nil {
return errors.Wrap(err, "failed to create balances at block")
}
p.newBalances = make(map[string]map[runes.RuneId]uint128.Uint128)
}
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
newRuneTxs := p.newRuneTxs
if err := runesDgTx.CreateRuneTransactions(ctx, newRuneTxs); err != nil {
return errors.Wrap(err, "failed to create rune transaction")
{
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)
}
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 {
if err := p.reportingClient.SubmitBlockReport(ctx, reportingclient.SubmitBlockReportPayload{
Type: "runes",
ClientVersion: constants.Version,
DBVersion: constants.DBVersion,
EventHashVersion: constants.EventHashVersion,
ClientVersion: Version,
DBVersion: DBVersion,
EventHashVersion: EventHashVersion,
Network: p.network,
BlockHeight: uint64(blockHeader.Height),
BlockHash: blockHeader.Hash,

View File

@@ -0,0 +1,130 @@
// 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

@@ -1,319 +0,0 @@
// 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

@@ -1,118 +0,0 @@
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.27.0
// sqlc v1.26.0
// source: data.sql
package gen
@@ -45,54 +45,6 @@ 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)
@@ -344,14 +296,12 @@ const getBalancesByPkScript = `-- name: GetBalancesByPkScript :many
WITH balances AS (
SELECT DISTINCT ON (rune_id) pkscript, block_height, rune_id, amount FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
)
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0
`
type GetBalancesByPkScriptParams struct {
Pkscript string
BlockHeight int32
Limit int32
Offset int32
}
type GetBalancesByPkScriptRow struct {
@@ -362,12 +312,7 @@ type GetBalancesByPkScriptRow struct {
}
func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getBalancesByPkScript,
arg.Pkscript,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
rows, err := q.db.Query(ctx, getBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
@@ -395,14 +340,12 @@ const getBalancesByRuneId = `-- name: GetBalancesByRuneId :many
WITH balances AS (
SELECT DISTINCT ON (pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
)
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0
`
type GetBalancesByRuneIdParams struct {
RuneID string
BlockHeight int32
Limit int32
Offset int32
}
type GetBalancesByRuneIdRow struct {
@@ -413,12 +356,7 @@ type GetBalancesByRuneIdRow struct {
}
func (q *Queries) GetBalancesByRuneId(ctx context.Context, arg GetBalancesByRuneIdParams) ([]GetBalancesByRuneIdRow, error) {
rows, err := q.db.Query(ctx, getBalancesByRuneId,
arg.RuneID,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
rows, err := q.db.Query(ctx, getBalancesByRuneId, arg.RuneID, arg.BlockHeight)
if err != nil {
return nil, err
}
@@ -476,120 +414,6 @@ 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
`
@@ -627,105 +451,6 @@ 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
@@ -906,106 +631,27 @@ func (q *Queries) GetRuneIdFromRune(ctx context.Context, rune string) (string, e
return rune_id, err
}
const getRuneTransaction = `-- name: GetRuneTransaction :one
SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.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 FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE hash = $1 LIMIT 1
`
type GetRuneTransactionRow struct {
Hash string
BlockHeight int32
Index int32
Timestamp pgtype.Timestamp
Inputs []byte
Outputs []byte
Mints []byte
Burns []byte
RuneEtched bool
TxHash pgtype.Text
BlockHeight_2 pgtype.Int4
Etching pgtype.Bool
EtchingDivisibility pgtype.Int2
EtchingPremine pgtype.Numeric
EtchingRune pgtype.Text
EtchingSpacers pgtype.Int4
EtchingSymbol pgtype.Int4
EtchingTerms pgtype.Bool
EtchingTermsAmount pgtype.Numeric
EtchingTermsCap pgtype.Numeric
EtchingTermsHeightStart pgtype.Int4
EtchingTermsHeightEnd pgtype.Int4
EtchingTermsOffsetStart pgtype.Int4
EtchingTermsOffsetEnd pgtype.Int4
EtchingTurbo pgtype.Bool
Edicts []byte
Mint pgtype.Text
Pointer pgtype.Int4
Cenotaph pgtype.Bool
Flaws pgtype.Int4
}
func (q *Queries) GetRuneTransaction(ctx context.Context, hash string) (GetRuneTransactionRow, error) {
row := q.db.QueryRow(ctx, getRuneTransaction, hash)
var i GetRuneTransactionRow
err := row.Scan(
&i.Hash,
&i.BlockHeight,
&i.Index,
&i.Timestamp,
&i.Inputs,
&i.Outputs,
&i.Mints,
&i.Burns,
&i.RuneEtched,
&i.TxHash,
&i.BlockHeight_2,
&i.Etching,
&i.EtchingDivisibility,
&i.EtchingPremine,
&i.EtchingRune,
&i.EtchingSpacers,
&i.EtchingSymbol,
&i.EtchingTerms,
&i.EtchingTermsAmount,
&i.EtchingTermsCap,
&i.EtchingTermsHeightStart,
&i.EtchingTermsHeightEnd,
&i.EtchingTermsOffsetStart,
&i.EtchingTermsOffsetEnd,
&i.EtchingTurbo,
&i.Edicts,
&i.Mint,
&i.Pointer,
&i.Cenotaph,
&i.Flaws,
)
return i, err
}
const getRuneTransactions = `-- name: GetRuneTransactions :many
SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.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 FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE (
$3::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR runes_transactions.outputs @> $2::JSONB
OR runes_transactions.inputs @> $2::JSONB
) AND (
$3::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $4::JSONB
OR runes_transactions.inputs @> $4::JSONB
OR runes_transactions.inputs @> $4::JSONB
OR runes_transactions.mints ? $5
OR runes_transactions.burns ? $5
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $6 AND runes_transactions.index = $7)
) AND (
$5::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $6::JSONB
OR runes_transactions.inputs @> $6::JSONB
OR runes_transactions.mints ? $7
OR runes_transactions.burns ? $7
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $8 AND runes_transactions.index = $9)
) AND (
$10 <= runes_transactions.block_height AND runes_transactions.block_height <= $11
$8 <= runes_transactions.block_height AND runes_transactions.block_height <= $9
)
ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2
ORDER BY runes_transactions.block_height DESC LIMIT 10000
`
type GetRuneTransactionsParams struct {
Limit int32
Offset int32
FilterPkScript bool
PkScriptParam []byte
FilterRuneID bool
@@ -1052,8 +698,6 @@ type GetRuneTransactionsRow struct {
func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactionsParams) ([]GetRuneTransactionsRow, error) {
rows, err := q.db.Query(ctx, getRuneTransactions,
arg.Limit,
arg.Offset,
arg.FilterPkScript,
arg.PkScriptParam,
arg.FilterRuneID,
@@ -1113,53 +757,32 @@ func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactio
return items, nil
}
const getRunesUTXOsByPkScript = `-- name: GetRunesUTXOsByPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
const getUnspentOutPointBalancesByPkScript = `-- name: GetUnspentOutPointBalancesByPkScript :many
SELECT rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height FROM runes_outpoint_balances WHERE pkscript = $1 AND block_height <= $2 AND (spent_height IS NULL OR spent_height > $2)
`
type GetRunesUTXOsByPkScriptParams struct {
Limit int32
Offset int32
type GetUnspentOutPointBalancesByPkScriptParams struct {
Pkscript string
BlockHeight int32
}
type GetRunesUTXOsByPkScriptRow struct {
TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByPkScript(ctx context.Context, arg GetRunesUTXOsByPkScriptParams) ([]GetRunesUTXOsByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
)
func (q *Queries) GetUnspentOutPointBalancesByPkScript(ctx context.Context, arg GetUnspentOutPointBalancesByPkScriptParams) ([]RunesOutpointBalance, error) {
rows, err := q.db.Query(ctx, getUnspentOutPointBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunesUTXOsByPkScriptRow
var items []RunesOutpointBalance
for rows.Next() {
var i GetRunesUTXOsByPkScriptRow
var i RunesOutpointBalance
if err := rows.Scan(
&i.RuneID,
&i.Pkscript,
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.RuneIds,
&i.Amounts,
&i.Amount,
&i.BlockHeight,
&i.SpentHeight,
); err != nil {
return nil, err
}
@@ -1171,116 +794,18 @@ func (q *Queries) GetRunesUTXOsByPkScript(ctx context.Context, arg GetRunesUTXOs
return items, nil
}
const getRunesUTXOsByRuneIdAndPkScript = `-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> $5::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
`
type GetRunesUTXOsByRuneIdAndPkScriptParams struct {
Limit int32
Offset int32
Pkscript string
BlockHeight int32
RuneIds []string
}
type GetRunesUTXOsByRuneIdAndPkScriptRow struct {
TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetRunesUTXOsByRuneIdAndPkScriptParams) ([]GetRunesUTXOsByRuneIdAndPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByRuneIdAndPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
arg.RuneIds,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunesUTXOsByRuneIdAndPkScriptRow
for rows.Next() {
var i GetRunesUTXOsByRuneIdAndPkScriptRow
if err := rows.Scan(
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.RuneIds,
&i.Amounts,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTotalHoldersByRuneIds = `-- name: GetTotalHoldersByRuneIds :many
WITH balances AS (
SELECT DISTINCT ON (rune_id, pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = ANY($1::TEXT[]) AND block_height <= $2 ORDER BY rune_id, pkscript, block_height DESC
)
SELECT rune_id, COUNT(DISTINCT pkscript) FROM balances WHERE amount > 0 GROUP BY rune_id
`
type GetTotalHoldersByRuneIdsParams struct {
RuneIds []string
BlockHeight int32
}
type GetTotalHoldersByRuneIdsRow struct {
RuneID string
Count int64
}
func (q *Queries) GetTotalHoldersByRuneIds(ctx context.Context, arg GetTotalHoldersByRuneIdsParams) ([]GetTotalHoldersByRuneIdsRow, error) {
rows, err := q.db.Query(ctx, getTotalHoldersByRuneIds, arg.RuneIds, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTotalHoldersByRuneIdsRow
for rows.Next() {
var i GetTotalHoldersByRuneIdsRow
if err := rows.Scan(&i.RuneID, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const spendOutPointBalance = `-- name: SpendOutPointBalance :exec
const spendOutPointBalances = `-- name: SpendOutPointBalances :exec
UPDATE runes_outpoint_balances SET spent_height = $1 WHERE tx_hash = $2 AND tx_idx = $3
`
type SpendOutPointBalanceParams struct {
type SpendOutPointBalancesParams struct {
SpentHeight pgtype.Int4
TxHash string
TxIdx int32
}
func (q *Queries) SpendOutPointBalance(ctx context.Context, arg SpendOutPointBalanceParams) error {
_, err := q.db.Exec(ctx, spendOutPointBalance, arg.SpentHeight, arg.TxHash, arg.TxIdx)
func (q *Queries) SpendOutPointBalances(ctx context.Context, arg SpendOutPointBalancesParams) error {
_, err := q.db.Exec(ctx, spendOutPointBalances, arg.SpentHeight, arg.TxHash, arg.TxIdx)
return err
}

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.26.0
package gen
@@ -15,6 +15,7 @@ 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.27.0
// sqlc v1.26.0
// source: info.sql
package gen

View File

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

View File

@@ -11,7 +11,6 @@ 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"
@@ -48,7 +47,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.UTC()
createdAt = src.CreatedAt.Time
}
return entity.IndexerState{
DBVersion: src.DbVersion,
@@ -64,7 +63,7 @@ func mapIndexerStateTypeToParams(src entity.IndexerState) gen.SetIndexerStatePar
}
}
func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error) {
func mapRuneEntryModelToType(src gen.GetRuneEntriesByRuneIdsRow) (runes.RuneEntry, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {
return runes.RuneEntry{}, errors.Wrap(err, "failed to parse rune id")
@@ -87,7 +86,7 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error)
}
var completedAt time.Time
if src.CompletedAt.Valid {
completedAt = src.CompletedAt.Time.UTC()
completedAt = src.CompletedAt.Time
}
var completedAtHeight *uint64
if src.CompletedAtHeight.Valid {
@@ -133,7 +132,7 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error)
}
var etchedAt time.Time
if src.EtchedAt.Valid {
etchedAt = src.EtchedAt.Time.UTC()
etchedAt = src.EtchedAt.Time
}
return runes.RuneEntry{
RuneId: runeId,
@@ -154,13 +153,31 @@ func mapRuneEntryModelToType(src gen.GetRuneEntriesRow) (runes.RuneEntry, error)
}, nil
}
func mapRuneEntryTypeToParams(src runes.RuneEntry) (gen.CreateRuneEntryParams, error) {
func mapRuneEntryTypeToParams(src runes.RuneEntry, blockHeight uint64) (gen.CreateRuneEntryParams, gen.CreateRuneEntryStateParams, 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{}, errors.Wrap(err, "failed to parse premine")
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
}
var terms bool
var termsAmount, termsCap pgtype.Numeric
@@ -170,13 +187,13 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry) (gen.CreateRuneEntryParams, e
if src.Terms.Amount != nil {
termsAmount, err = numericFromUint128(src.Terms.Amount)
if err != nil {
return gen.CreateRuneEntryParams{}, errors.Wrap(err, "failed to parse terms amount")
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, 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{}, errors.Wrap(err, "failed to parse terms cap")
return gen.CreateRuneEntryParams{}, gen.CreateRuneEntryStateParams{}, errors.Wrap(err, "failed to parse terms cap")
}
}
if src.Terms.HeightStart != nil {
@@ -204,150 +221,51 @@ func mapRuneEntryTypeToParams(src runes.RuneEntry) (gen.CreateRuneEntryParams, e
}
}
}
etchedAt := pgtype.Timestamp{Time: src.EtchedAt.UTC(), Valid: true}
etchedAt := pgtype.Timestamp{Time: time.Time{}, 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,
}, 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,
}, gen.CreateRuneEntryStateParams{
BlockHeight: int32(blockHeight),
RuneID: runeId,
Mints: mints,
BurnedAmount: burnedAmount,
CompletedAt: completedAt,
CompletedAtHeight: completedAtHeight,
}, nil
}
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) {
// mapRuneTransactionModelToType returns params for creating a new rune transaction and (optionally) a runestone.
func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneTransactionParams, *gen.CreateRunestoneParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp.Time = src.Timestamp.UTC()
timestamp.Time = src.Timestamp
timestamp.Valid = true
}
inputsBytes, err := json.Marshal(src.Inputs)
if err != nil {
return gen.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal inputs")
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal inputs")
}
outputsBytes, err := json.Marshal(src.Outputs)
if err != nil {
return gen.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal outputs")
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal outputs")
}
mints := make(map[string]uint128.Uint128)
for key, value := range src.Mints {
@@ -355,7 +273,7 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}
mintsBytes, err := json.Marshal(mints)
if err != nil {
return gen.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal mints")
return gen.CreateRuneTransactionParams{}, nil, errors.Wrap(err, "failed to marshal mints")
}
burns := make(map[string]uint128.Uint128)
for key, value := range src.Burns {
@@ -363,7 +281,16 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}
burnsBytes, err := json.Marshal(burns)
if err != nil {
return gen.CreateRuneTransactionParams{}, errors.Wrap(err, "failed to marshal burns")
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{
@@ -376,46 +303,7 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
Mints: mintsBytes,
Burns: burnsBytes,
RuneEtched: src.RuneEtched,
}, 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
}, runestoneParams, nil
}
func extractModelRuneTxAndRunestone(src gen.GetRuneTransactionsRow) (gen.RunesTransaction, *gen.RunesRunestone, error) {
@@ -600,65 +488,6 @@ 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,
@@ -773,42 +602,6 @@ 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 {
@@ -845,72 +638,6 @@ func mapIndexedBlockTypeToParams(src entity.IndexedBlock) (gen.CreateIndexedBloc
}, nil
}
func mapRunesUTXOModelToType(src gen.GetRunesUTXOsByPkScriptRow) (entity.RunesUTXO, error) {
pkScriptRaw, ok := src.Pkscript.(string)
if !ok {
return entity.RunesUTXO{}, errors.New("pkscript from database is not string")
}
pkScript, err := hex.DecodeString(pkScriptRaw)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse pkscript")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse tx hash")
}
runeIdsRaw, ok := src.RuneIds.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("src.RuneIds is not a slice")
}
runeIds := make([]string, 0, len(runeIdsRaw))
for i, raw := range runeIdsRaw {
runeId, ok := raw.(string)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.RuneIds[%d] is not a string", i)
}
runeIds = append(runeIds, runeId)
}
amountsRaw, ok := src.Amounts.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("amounts from database is not a slice")
}
amounts := make([]pgtype.Numeric, 0, len(amountsRaw))
for i, raw := range amountsRaw {
amount, ok := raw.(pgtype.Numeric)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.Amounts[%d] is not pgtype.Numeric", i)
}
amounts = append(amounts, amount)
}
if len(runeIds) != len(amounts) {
return entity.RunesUTXO{}, errors.New("rune ids and amounts have different lengths")
}
runesBalances := make([]entity.RunesUTXOBalance, 0, len(runeIds))
for i := range runeIds {
runeId, err := runes.NewRuneIdFromString(runeIds[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse rune id")
}
amount, err := uint128FromNumeric(amounts[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse amount")
}
runesBalances = append(runesBalances, entity.RunesUTXOBalance{
RuneId: runeId,
Amount: lo.FromPtr(amount),
})
}
return entity.RunesUTXO{
PkScript: pkScript,
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.TxIdx),
},
RuneBalances: runesBalances,
}, nil
}
func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPointBalance, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {
@@ -945,16 +672,16 @@ func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPoin
}, nil
}
func mapOutPointBalanceTypeToParams(src entity.OutPointBalance) (gen.CreateOutPointBalanceParams, error) {
func mapOutPointBalanceTypeToParams(src entity.OutPointBalance) (gen.CreateOutPointBalancesParams, error) {
amount, err := numericFromUint128(&src.Amount)
if err != nil {
return gen.CreateOutPointBalanceParams{}, errors.Wrap(err, "failed to parse amount")
return gen.CreateOutPointBalancesParams{}, 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.CreateOutPointBalanceParams{
return gen.CreateOutPointBalancesParams{
TxHash: src.OutPoint.Hash.String(),
TxIdx: int32(src.OutPoint.Index),
Pkscript: hex.EncodeToString(src.PkScript),
@@ -964,31 +691,3 @@ 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

@@ -4,7 +4,6 @@ import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@@ -63,18 +62,7 @@ func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64)
return indexedBlock, nil
}
const maxRuneTransactionsLimit = 10000 // temporary limit to prevent large queries from overwhelming the database
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
if limit == -1 {
limit = maxRuneTransactionsLimit
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
if limit > maxRuneTransactionsLimit {
return nil, errors.Wrapf(errs.InvalidArgument, "limit cannot exceed %d", maxRuneTransactionsLimit)
}
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) {
pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript)))
runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String()))
rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{
@@ -89,9 +77,6 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
FromBlock: int32(fromBlock),
ToBlock: int32(toBlock),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
@@ -120,36 +105,6 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
return runeTxs, nil
}
func (r *Repository) GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error) {
row, err := r.queries.GetRuneTransaction(ctx, txHash.String())
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))
if err != nil {
return nil, errors.Wrap(err, "failed to extract rune transaction and runestone from row")
}
runeTx, err := mapRuneTransactionModelToType(runeTxModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse rune transaction model")
}
if runestoneModel != nil {
runestone, err := mapRunestoneModelToType(*runestoneModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse runestone model")
}
runeTx.Runestone = &runestone
}
return &runeTx, nil
}
func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error) {
balances, err := r.queries.GetOutPointBalancesAtOutPoint(ctx, gen.GetOutPointBalancesAtOutPointParams{
TxHash: outPoint.Hash.String(),
@@ -170,59 +125,22 @@ func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wi
return result, nil
}
func (r *Repository) GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByPkScript(ctx, gen.GetRunesUTXOsByPkScriptParams{
func (r *Repository) GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error) {
balances, err := r.queries.GetUnspentOutPointBalancesByPkScript(ctx, gen.GetUnspentOutPointBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.RunesUTXO, 0, len(rows))
for _, row := range rows {
utxo, err := mapRunesUTXOModelToType(row)
result := make([]*entity.OutPointBalance, 0, len(balances))
for _, balanceModel := range balances {
balance, err := mapOutPointBalanceModelToType(balanceModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse row model")
return nil, errors.Wrap(err, "failed to parse balance model")
}
result = append(result, &utxo)
}
return result, nil
}
func (r *Repository) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByRuneIdAndPkScript(ctx, gen.GetRunesUTXOsByRuneIdAndPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
RuneIds: []string{runeId.String()},
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.RunesUTXO, 0, len(rows))
for _, row := range rows {
utxo, err := mapRunesUTXOModelToType(gen.GetRunesUTXOsByPkScriptRow(row))
if err != nil {
return nil, errors.Wrap(err, "failed to parse row")
}
result = append(result, &utxo)
result = append(result, &balance)
}
return result, nil
}
@@ -265,7 +183,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(gen.GetRuneEntriesRow(runeEntryModel))
runeEntry, err := mapRuneEntryModelToType(runeEntryModel)
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
@@ -305,7 +223,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.GetRuneEntriesRow(runeEntryModel))
runeEntry, err := mapRuneEntryModelToType(gen.GetRuneEntriesByRuneIdsRow(runeEntryModel))
if err != nil {
errs = append(errs, errors.Wrapf(err, "failed to parse rune entry model index %d", i))
continue
@@ -319,62 +237,6 @@ 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 {
@@ -383,46 +245,30 @@ func (r *Repository) CountRuneEntries(ctx context.Context) (uint64, error) {
return uint64(count), nil
}
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) {
balances, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.Balance, 0, len(balances))
result := make(map[runes.RuneId]*entity.Balance, len(balances))
for _, balanceModel := range balances {
balance, err := mapBalanceModelToType(gen.RunesBalance(balanceModel))
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model")
}
result = append(result, balance)
result[balance.RuneId] = balance
}
return result, nil
}
func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) {
balances, err := r.queries.GetBalancesByRuneId(ctx, gen.GetBalancesByRuneIdParams{
RuneID: runeId.String(),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
@@ -459,135 +305,111 @@ func (r *Repository) GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript
return result, nil
}
func (r *Repository) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]int64, error) {
rows, err := r.queries.GetTotalHoldersByRuneIds(ctx, gen.GetTotalHoldersByRuneIdsParams{
RuneIds: lo.Map(runeIds, func(runeId runes.RuneId, _ int) string { return runeId.String() }),
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
func (r *Repository) CreateRuneTransaction(ctx context.Context, tx *entity.RuneTransaction) error {
if tx == nil {
return nil
}
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")
txParams, runestoneParams, err := mapRuneTransactionTypeToParams(*tx)
if err != nil {
return errors.Wrap(err, "failed to map rune transaction to params")
}
if err = r.queries.CreateRuneTransaction(ctx, txParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneTransaction")
}
if runestoneParams != nil {
if err = r.queries.CreateRunestone(ctx, *runestoneParams); err != nil {
return errors.Wrap(err, "error during exec CreateRunestone")
}
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) CreateRuneEntries(ctx context.Context, entries []*runes.RuneEntry) error {
if len(entries) == 0 {
func (r *Repository) CreateRuneEntry(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
return nil
}
params, err := mapRuneEntryTypeToParamsBatch(entries)
createParams, _, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entries to params")
return errors.Wrap(err, "failed to map rune entry to params")
}
if err := r.queries.BatchCreateRuneEntriesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
if err = r.queries.CreateRuneEntry(ctx, createParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntry")
}
return nil
}
func (r *Repository) CreateRuneEntryStates(ctx context.Context, entries []*runes.RuneEntry, blockHeight uint64) error {
if len(entries) == 0 {
func (r *Repository) CreateRuneEntryState(ctx context.Context, entry *runes.RuneEntry, blockHeight uint64) error {
if entry == nil {
return nil
}
params, err := mapRuneEntryStatesTypeToParamsBatch(entries, blockHeight)
_, createStateParams, err := mapRuneEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "failed to map rune entry states to params")
return errors.Wrap(err, "failed to map rune entry to params")
}
if err := r.queries.BatchCreateRuneEntryStatesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
if err = r.queries.CreateRuneEntryState(ctx, createStateParams); err != nil {
return errors.Wrap(err, "error during exec CreateRuneEntryState")
}
return nil
}
func (r *Repository) CreateOutPointBalances(ctx context.Context, outPointBalances []*entity.OutPointBalance) error {
if len(outPointBalances) == 0 {
return nil
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)
}
params, err := mapOutPointBalanceTypeToParamsBatch(outPointBalances)
if err != nil {
return errors.Wrap(err, "failed to map outpoint balances to params")
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")
}
if err := r.queries.BatchCreateRunesOutpointBalancesPatched(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) SpendOutPointBalancesBatch(ctx context.Context, outPoints []wire.OutPoint, blockHeight uint64) error {
if len(outPoints) == 0 {
return nil
}
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 {
func (r *Repository) SpendOutPointBalances(ctx context.Context, outPoint wire.OutPoint, blockHeight uint64) error {
if err := r.queries.SpendOutPointBalances(ctx, gen.SpendOutPointBalancesParams{
TxHash: outPoint.Hash.String(),
TxIdx: int32(outPoint.Index),
SpentHeight: pgtype.Int4{Int32: int32(blockHeight), Valid: true},
}); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) CreateRuneBalances(ctx context.Context, balances []*entity.Balance) error {
if len(balances) == 0 {
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,
})
}
params, err := mapBalanceTypeToParamsBatch(balances)
if err != nil {
return errors.Wrap(err, "failed to map rune balances to params")
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")
}
if err := r.queries.BatchCreateRunesBalances(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}

View File

@@ -67,11 +67,10 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
}
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient, cleanupFuncs)
if !conf.APIOnly {
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
}
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

@@ -5,7 +5,6 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/uint128"
)
@@ -59,8 +58,7 @@ func ParseFlags(input interface{}) (Flags, error) {
}
return Flags(u128), nil
default:
logger.Panic("invalid flags input type")
return Flags{}, nil
panic("invalid flags input type")
}
}

View File

@@ -1,13 +1,11 @@
package runes
import (
"fmt"
"slices"
"github.com/Cleverse/go-utilities/utils"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/uint128"
)
@@ -31,10 +29,6 @@ var ErrInvalidBase26 = errors.New("invalid base-26 character: must be in the ran
func NewRuneFromString(value string) (Rune, error) {
n := uint128.From64(0)
for i, char := range value {
// skip spacers
if char == '.' || char == '•' {
continue
}
if i > 0 {
n = n.Add(uint128.From64(1))
}
@@ -121,25 +115,20 @@ func (r Rune) Cmp(other Rune) int {
func FirstRuneHeight(network common.Network) uint64 {
switch network {
case common.NetworkMainnet:
return 840_000
return common.HalvingInterval * 4
case common.NetworkTestnet:
return 2_520_000
case common.NetworkFractalMainnet:
return 84_000
case common.NetworkFractalTestnet:
return 84_000
return common.HalvingInterval * 12
}
logger.Panic(fmt.Sprintf("invalid network: %s", network))
return 0
panic("invalid network")
}
func MinimumRuneAtHeight(network common.Network, height uint64) Rune {
offset := height + 1
interval := network.HalvingInterval() / 12
interval := common.HalvingInterval / 12
// runes are gradually unlocked from rune activation height until the next halving
start := FirstRuneHeight(network)
end := start + network.HalvingInterval()
end := start + common.HalvingInterval
if offset < start {
return (Rune)(unlockSteps[12])

View File

@@ -92,8 +92,8 @@ func TestMinimumRuneAtHeightMainnet(t *testing.T) {
}
start := FirstRuneHeight(common.NetworkMainnet)
end := start + common.NetworkMainnet.HalvingInterval()
interval := uint64(common.NetworkMainnet.HalvingInterval() / 12)
end := start + common.HalvingInterval
interval := uint64(common.HalvingInterval / 12)
test(0, "AAAAAAAAAAAAA")
test(start/2, "AAAAAAAAAAAAA")

View File

@@ -5,7 +5,6 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/uint128"
)
@@ -103,7 +102,6 @@ func ParseTag(input interface{}) (Tag, error) {
}
return Tag(u128), nil
default:
logger.Panic("invalid tag input type")
return Tag{}, nil
panic("invalid tag input type")
}
}

View File

@@ -1,5 +0,0 @@
package usecase
import "github.com/cockroachdb/errors"
var ErrUTXONotFound = errors.New("utxo not found")

View File

@@ -8,38 +8,18 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/runes"
)
// Use limit = -1 as no limit.
func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByPkScript(ctx, pkScript, blockHeight, limit, offset)
func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByPkScript(ctx, pkScript, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
return balances, nil
}
// Use limit = -1 as no limit.
func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByRuneId(ctx, runeId, blockHeight, limit, offset)
func (u *Usecase) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) {
balances, err := u.runesDg.GetBalancesByRuneId(ctx, runeId, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "failed to get rune holders by rune id")
}
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

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

View File

@@ -40,25 +40,9 @@ 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) {
runeEntries, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
runeEntry, 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 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
return runeEntry, nil
}

View File

@@ -3,25 +3,15 @@ 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"
)
// Use limit = -1 as no limit.
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock, limit, offset)
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock)
if err != nil {
return nil, errors.Wrap(err, "error during GetTransactionsByHeight")
}
return txs, nil
}
func (u *Usecase) GetRuneTransaction(ctx context.Context, hash chainhash.Hash) (*entity.RuneTransaction, error) {
tx, err := u.runesDg.GetRuneTransaction(ctx, hash)
if err != nil {
return nil, errors.Wrap(err, "error during GetRuneTransaction")
}
return tx, nil
}

View File

@@ -1,119 +0,0 @@
package usecase
import (
"context"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"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"
)
func (u *Usecase) GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXOWithSats, error) {
balances, err := u.runesDg.GetRunesUTXOsByPkScript(ctx, pkScript, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
result := make([]*entity.RunesUTXOWithSats, 0, len(balances))
for _, balance := range balances {
tx, err := u.bitcoinClient.GetRawTransactionByTxHash(ctx, balance.OutPoint.Hash)
if err != nil {
if strings.Contains(err.Error(), "No such mempool or blockchain transaction.") {
return nil, errors.WithStack(ErrUTXONotFound)
}
return nil, errors.WithStack(err)
}
result = append(result, &entity.RunesUTXOWithSats{
RunesUTXO: entity.RunesUTXO{
PkScript: balance.PkScript,
OutPoint: balance.OutPoint,
RuneBalances: balance.RuneBalances,
},
Sats: tx.TxOut[balance.OutPoint.Index].Value,
})
}
return result, nil
}
func (u *Usecase) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXOWithSats, error) {
balances, err := u.runesDg.GetRunesUTXOsByRuneIdAndPkScript(ctx, runeId, pkScript, blockHeight, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
}
result := make([]*entity.RunesUTXOWithSats, 0, len(balances))
for _, balance := range balances {
tx, err := u.bitcoinClient.GetRawTransactionByTxHash(ctx, balance.OutPoint.Hash)
if err != nil {
if strings.Contains(err.Error(), "No such mempool or blockchain transaction.") {
return nil, errors.WithStack(ErrUTXONotFound)
}
return nil, errors.WithStack(err)
}
result = append(result, &entity.RunesUTXOWithSats{
RunesUTXO: entity.RunesUTXO{
PkScript: balance.PkScript,
OutPoint: balance.OutPoint,
RuneBalances: balance.RuneBalances,
},
Sats: tx.TxOut[balance.OutPoint.Index].Value,
})
}
return result, nil
}
func (u *Usecase) GetUTXOsOutputByLocation(ctx context.Context, txHash chainhash.Hash, outputIdx uint32) (*entity.RunesUTXOWithSats, error) {
tx, err := u.bitcoinClient.GetRawTransactionByTxHash(ctx, txHash)
if err != nil {
if strings.Contains(err.Error(), "No such mempool or blockchain transaction.") {
return nil, errors.WithStack(ErrUTXONotFound)
}
return nil, errors.WithStack(err)
}
// If the output index is out of range, return an error
if len(tx.TxOut) <= int(outputIdx) {
return nil, errors.WithStack(ErrUTXONotFound)
}
rune := &entity.RunesUTXOWithSats{
RunesUTXO: entity.RunesUTXO{
PkScript: tx.TxOut[0].PkScript,
OutPoint: wire.OutPoint{
Hash: txHash,
Index: outputIdx,
},
},
Sats: tx.TxOut[outputIdx].Value,
}
transaction, err := u.runesDg.GetRuneTransaction(ctx, txHash)
// If Bitcoin transaction is not found in the database, return the PkScript and OutPoint
if errors.Is(err, errs.NotFound) {
return rune, nil
}
if err != nil {
return nil, errors.WithStack(err)
}
runeBalance := make([]entity.RunesUTXOBalance, 0, len(transaction.Outputs))
for _, output := range transaction.Outputs {
if output.Index == outputIdx {
runeBalance = append(runeBalance, entity.RunesUTXOBalance{
RuneId: output.RuneId,
Amount: output.Amount,
})
}
}
rune.RuneBalances = runeBalance
return rune, nil
}

View File

@@ -1,191 +0,0 @@
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
}

View File

@@ -1,145 +0,0 @@
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)
})
}
}

View File

@@ -1,77 +0,0 @@
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

@@ -9,6 +9,4 @@ import (
type Contract interface {
GetRawTransactionAndHeightByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, int64, error)
GetRawTransactionByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, error)
}

View File

@@ -150,18 +150,6 @@ func (a Address) Equal(b Address) bool {
return a.encoded == b.encoded
}
// DustLimit returns the output dust limit (lowest possible satoshis in a UTXO) for the address type.
func (a Address) DustLimit() int64 {
switch a.encodedType {
case AddressP2TR:
return 330
case AddressP2WPKH:
return 294
default:
return 546
}
}
// MarshalText implements the encoding.TextMarshaler interface.
func (a Address) MarshalText() ([]byte, error) {
return []byte(a.encoded), nil

View File

@@ -447,72 +447,3 @@ func TestAddressPkScript(t *testing.T) {
})
}
}
func TestAddressDustLimit(t *testing.T) {
type Spec struct {
Address string
DefaultNet *chaincfg.Params
ExpectedDustLimit int64
}
specs := []Spec{
{
Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 294,
},
{
Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 294,
},
{
Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 330,
},
{
Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 330,
},
{
Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 546,
},
{
Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 546,
},
{
Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 546,
},
{
Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3",
DefaultNet: &chaincfg.TestNet3Params,
ExpectedDustLimit: 546,
},
{
Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
DefaultNet: &chaincfg.MainNetParams,
ExpectedDustLimit: 546,
},
{
Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ",
DefaultNet: &chaincfg.TestNet3Params,
ExpectedDustLimit: 546,
},
}
for _, spec := range specs {
t.Run(spec.Address, func(t *testing.T) {
addr, err := btcutils.SafeNewAddress(spec.Address, spec.DefaultNet)
require.NoError(t, err)
assert.Equal(t, spec.ExpectedDustLimit, addr.DustLimit())
})
}
}

View File

@@ -1,56 +0,0 @@
package btcutils
import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
// EstimateSignedTxNetworkFee estimates the network fee for the given transaction. "prevTxOuts" should be list of all outputs used as inputs in the transaction.
// If the transaction has unsigned inputs, the fee will be calculated as if those inputs were signed.
func EstimateSignedTxNetworkFee(tx *wire.MsgTx, prevTxOuts []*wire.TxOut, feeRate int64) (int64, error) {
if len(tx.TxIn) != len(prevTxOuts) {
return 0, errors.Wrapf(errs.InvalidArgument, "tx.TxIn length (%d) must match prevTxOuts length (%d)", len(tx.TxIn), len(prevTxOuts))
}
tx = tx.Copy()
mockPrivateKey, _ := btcec.NewPrivateKey()
for i := range tx.TxIn {
if len(tx.TxIn[i].SignatureScript) > 0 || (len(tx.TxIn[i].Witness) > 0 && len(tx.TxIn[i].Witness[0]) > 0) {
// already signed, skip
continue
}
address, err := ExtractAddressFromPkScript(prevTxOuts[i].PkScript)
if err != nil {
return 0, errors.Wrapf(err, "failed to extract address from pkScript %d", i)
}
// if the input is a taproot script-path spend, we need to sign it with the tapscript
if address.Type() == AddressTaproot && len(tx.TxIn[i].Witness) == 3 {
tx, err = SignTxInputTapScript(tx, mockPrivateKey, prevTxOuts[i], i)
if err != nil {
return 0, errors.Wrapf(err, "failed to sign tx input %d (tapscript)", i)
}
} else {
tx, err = SignTxInput(tx, mockPrivateKey, prevTxOuts[i], i)
if err != nil {
return 0, errors.Wrapf(err, "failed to sign tx input %d", i)
}
}
}
txWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx))
txVBytes := calVBytes(txWeight)
fee := txVBytes * feeRate
return fee, nil
}
func calVBytes(txWeight int64) int64 {
// VBytes = txWeight/4, a fraction of Vbyte uses 1 Vbyte.
txVBytes := txWeight / 4
if txWeight%4 > 0 {
txVBytes += 1
}
return txVBytes
}

View File

@@ -3,12 +3,8 @@ package btcutils
import (
"github.com/Cleverse/go-utilities/utils"
verifier "github.com/bitonicnl/verify-signed-message/pkg"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
func VerifySignature(address string, message string, sigBase64 string, defaultNet ...*chaincfg.Params) error {
@@ -23,133 +19,3 @@ func VerifySignature(address string, message string, sigBase64 string, defaultNe
}
return nil
}
type SignTxInputOptions struct {
SigHashType txscript.SigHashType
}
func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.TxOut, inputIndex int, options ...SignTxInputOptions) (*wire.MsgTx, error) {
if privateKey == nil {
return nil, errors.Wrap(errs.InvalidArgument, "PrivateKey is required")
}
if tx == nil {
return nil, errors.Wrap(errs.InvalidArgument, "Tx is required")
}
if prevTxOut == nil {
return nil, errors.Wrap(errs.InvalidArgument, "PrevTxOut is required")
}
// defaults sigHashType to SigHashAll | SigHashAnyOneCanPay
if len(options) == 0 {
options = append(options, SignTxInputOptions{
SigHashType: txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
})
}
sigHashType := options[0].SigHashType
prevOutFetcher := txscript.NewCannedPrevOutputFetcher(prevTxOut.PkScript, prevTxOut.Value)
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
if len(tx.TxIn) <= inputIndex {
return nil, errors.Errorf("input to sign (%d) is out of range", inputIndex)
}
address, err := ExtractAddressFromPkScript(prevTxOut.PkScript)
if err != nil {
return nil, errors.Wrap(err, "failed to extract address")
}
switch address.Type() {
case AddressP2TR:
witness, err := txscript.TaprootWitnessSignature(
tx,
sigHashes,
inputIndex,
prevTxOut.Value,
prevTxOut.PkScript,
sigHashType,
privateKey)
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
}
tx.TxIn[inputIndex].Witness = witness
case AddressP2WPKH:
witness, err := txscript.WitnessSignature(
tx,
sigHashes,
inputIndex,
prevTxOut.Value,
prevTxOut.PkScript,
sigHashType,
privateKey,
true,
)
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
}
tx.TxIn[inputIndex].Witness = witness
case AddressP2PKH:
sigScript, err := txscript.SignatureScript(
tx,
inputIndex,
prevTxOut.PkScript,
sigHashType,
privateKey,
true,
)
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
}
tx.TxIn[inputIndex].SignatureScript = sigScript
default:
return nil, errors.Wrapf(errs.NotSupported, "unsupported input address type %s", address.Type())
}
return tx, nil
}
func SignTxInputTapScript(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.TxOut, inputIndex int) (*wire.MsgTx, error) {
if privateKey == nil {
return nil, errors.Wrap(errs.InvalidArgument, "PrivateKey is required")
}
if tx == nil {
return nil, errors.Wrap(errs.InvalidArgument, "Tx is required")
}
if prevTxOut == nil {
return nil, errors.Wrap(errs.InvalidArgument, "PrevTxOut is required")
}
prevOutFetcher := txscript.NewCannedPrevOutputFetcher(prevTxOut.PkScript, prevTxOut.Value)
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
if len(tx.TxIn) <= inputIndex {
return nil, errors.Errorf("input to sign (%d) is out of range", inputIndex)
}
address, err := ExtractAddressFromPkScript(prevTxOut.PkScript)
if err != nil {
return nil, errors.Wrap(err, "failed to extract address")
}
if address.Type() != AddressTaproot {
return nil, errors.Errorf("input type must be %s", AddressTaproot)
}
witness := tx.TxIn[inputIndex].Witness
if len(witness) != 3 {
return nil, errors.Wrapf(errs.InvalidArgument, "invalid witness length: expected 3, got %d", len(witness))
}
tapLeaf := txscript.NewBaseTapLeaf(witness[1])
signature, err := txscript.RawTxInTapscriptSignature(
tx,
sigHashes,
inputIndex,
prevTxOut.Value,
prevTxOut.PkScript,
tapLeaf,
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
privateKey)
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
}
tx.TxIn[inputIndex].Witness[0] = signature
return tx, nil
}

View File

@@ -3,14 +3,8 @@ package btcutils
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifySignature(t *testing.T) {
@@ -73,175 +67,3 @@ func TestVerifySignature(t *testing.T) {
assert.Error(t, err)
}
}
func TestSignTxInput(t *testing.T) {
generateTxAndPrevTxOutFromPkScript := func(pkScript []byte) (*wire.MsgTx, *wire.TxOut) {
tx := wire.NewMsgTx(wire.TxVersion)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Index: 1,
},
})
txOut := &wire.TxOut{
Value: 1e8, PkScript: pkScript,
}
tx.AddTxOut(txOut)
// using same value and pkScript as input for simplicity
return tx, txOut
}
verifySignedTx := func(t *testing.T, signedTx *wire.MsgTx, prevTxOut *wire.TxOut) {
t.Helper()
prevOutFetcher := txscript.NewCannedPrevOutputFetcher(prevTxOut.PkScript, prevTxOut.Value)
sigHashes := txscript.NewTxSigHashes(signedTx, prevOutFetcher)
vm, err := txscript.NewEngine(
prevTxOut.PkScript, signedTx, 0, txscript.StandardVerifyFlags,
nil, sigHashes, prevTxOut.Value, prevOutFetcher,
)
require.NoError(t, err)
require.NoError(t, vm.Execute(), "error during signature verification") // no error means success
}
privKey, _ := btcec.NewPrivateKey()
t.Run("P2TR input", func(t *testing.T) {
taprootKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey())
pkScript, err := txscript.PayToTaprootScript(taprootKey)
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
signedTx, err := SignTxInput(
tx, privKey, prevTxOut, 0,
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
})
t.Run("tapscript input", func(t *testing.T) {
internalKey := privKey.PubKey()
// Our script will be a simple OP_CHECKSIG as the sole leaf of a
// tapscript tree.
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(internalKey))
builder.AddOp(txscript.OP_CHECKSIG)
tapScript, err := builder.Script()
require.NoError(t, err)
tapLeaf := txscript.NewBaseTapLeaf(tapScript)
tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)
controlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(
internalKey,
)
controlBlockBytes, err := controlBlock.ToBytes()
require.NoError(t, err)
tapScriptRootHash := tapScriptTree.RootNode.TapHash()
outputKey := txscript.ComputeTaprootOutputKey(
internalKey, tapScriptRootHash[:],
)
p2trScript, err := txscript.PayToTaprootScript(outputKey)
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(p2trScript)
tx.TxIn[0].Witness = wire.TxWitness{
{},
tapScript,
controlBlockBytes,
}
signedTx, err := SignTxInputTapScript(
tx, privKey, prevTxOut, 0,
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
})
t.Run("P2WPKH input", func(t *testing.T) {
pubKey := privKey.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
pkScript, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(pubKeyHash).
Script()
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
signedTx, err := SignTxInput(
tx, privKey, prevTxOut, 0,
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
})
t.Run("P2PKH input", func(t *testing.T) {
pubKey := privKey.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
address, err := btcutil.NewAddressPubKeyHash(pubKeyHash, &chaincfg.MainNetParams)
require.NoError(t, err)
pkScript, err := txscript.PayToAddrScript(address)
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
signedTx, err := SignTxInput(
tx, privKey, prevTxOut, 0,
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
})
t.Run("custom sighash type", func(t *testing.T) {
pubKey := privKey.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
pkScript, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(pubKeyHash).
Script()
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
sigHashTypes := []txscript.SigHashType{
txscript.SigHashAll,
txscript.SigHashNone,
txscript.SigHashSingle,
txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
txscript.SigHashNone | txscript.SigHashAnyOneCanPay,
txscript.SigHashSingle | txscript.SigHashAnyOneCanPay,
}
for _, sigHashType := range sigHashTypes {
signedTx, err := SignTxInput(
tx, privKey, prevTxOut, 0, SignTxInputOptions{
SigHashType: sigHashType,
},
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
// check last byte of signature equals to sigHashType
signature := signedTx.TxIn[0].Witness[0]
assert.Equal(t, sigHashType, txscript.SigHashType(signature[len(signature)-1]))
}
})
t.Run("default sighash type", func(t *testing.T) {
pubKey := privKey.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
pkScript, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(pubKeyHash).
Script()
require.NoError(t, err)
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
signedTx, err := SignTxInput(
tx, privKey, prevTxOut, 0,
)
require.NoError(t, err)
verifySignedTx(t, signedTx, prevTxOut)
// check last byte of signature equals to sigHashType
signature := signedTx.TxIn[0].Witness[0]
expected := txscript.SigHashAll | txscript.SigHashAnyOneCanPay
assert.Equal(t, expected, txscript.SigHashType(signature[len(signature)-1]))
})
}

View File

@@ -14,12 +14,6 @@ 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
@@ -70,14 +64,13 @@ func (r *HttpResponse) UnmarshalBody(out any) error {
if err != nil {
return errors.Wrapf(err, "can't uncompress body from %v", r.URL)
}
contentType := strings.ToLower(string(r.Header.ContentType()))
switch {
case strings.Contains(contentType, "application/json"):
switch strings.ToLower(string(r.Header.ContentType())) {
case "application/json", "application/json; charset=utf-8":
if err := json.Unmarshal(body, out); err != nil {
return errors.Wrapf(err, "can't unmarshal json body from %s, %q", r.URL, string(body))
}
return nil
case strings.Contains(contentType, "text/plain"):
case "text/plain", "text/plain; charset=utf-8":
return errors.Errorf("can't unmarshal plain text %q", string(body))
default:
return errors.Errorf("unsupported content type: %s, contents: %v", r.Header.ContentType(), string(r.Body()))
@@ -97,10 +90,6 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
parsedUrl := h.BaseURL()
parsedUrl.Path = path.Join(parsedUrl.Path, reqOptions.path)
// Because path.Join cleans the joined path. If path ends with /, append "/" to parsedUrl.Path
if strings.HasSuffix(reqOptions.path, "/") && !strings.HasSuffix(parsedUrl.Path, "/") {
parsedUrl.Path += "/"
}
baseQuery := parsedUrl.Query()
for k, v := range reqOptions.Query {
baseQuery[k] = v
@@ -149,7 +138,7 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
fasthttp.ReleaseRequest(req)
}()
if err := DefaultClient.Do(req, resp); err != nil {
if err := fasthttp.Do(req, resp); err != nil {
return nil, errors.Wrapf(err, "url: %s", url)
}