mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-03-26 06:54:23 +08:00
Compare commits
29 Commits
v0.4.2
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5e742cfa5 | ||
|
|
10d31de197 | ||
|
|
eab0eb5ac3 | ||
|
|
1d2cc88f90 | ||
|
|
8a1c096656 | ||
|
|
17ab11200a | ||
|
|
1358b96165 | ||
|
|
2c0a01da56 | ||
|
|
a5651f9812 | ||
|
|
df59e5fb46 | ||
|
|
43a85c5171 | ||
|
|
5574db8691 | ||
|
|
5497ef85c6 | ||
|
|
28c42c02b7 | ||
|
|
38cf4c95a5 | ||
|
|
b8ed647d91 | ||
|
|
9f327c58fa | ||
|
|
dd35ab125e | ||
|
|
276b20abca | ||
|
|
d62445887b | ||
|
|
cbc6bde3b5 | ||
|
|
f150451dd7 | ||
|
|
0b09724c71 | ||
|
|
237d0f0d73 | ||
|
|
b10b8c59d7 | ||
|
|
6be4c877d6 | ||
|
|
c86380718f | ||
|
|
1e462a09d9 | ||
|
|
338d4d47b3 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
4
common/bitcoin.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package common
|
||||
|
||||
// HalvingInterval is the number of blocks between each halving event.
|
||||
const HalvingInterval = 210_000
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"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 {
|
||||
type getBalancesByAddressRequest struct {
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
|
||||
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"))
|
||||
@@ -31,12 +25,6 @@ func (r getBalancesRequest) Validate() error {
|
||||
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
}
|
||||
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))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
@@ -48,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)
|
||||
}
|
||||
@@ -66,9 +54,6 @@ func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getBalancesDefaultLimit
|
||||
}
|
||||
|
||||
pkScript, ok := resolvePkScript(h.network, req.Wallet)
|
||||
if !ok {
|
||||
@@ -79,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,
|
||||
},
|
||||
|
||||
@@ -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' is not valid rune id or rune name", i))
|
||||
}
|
||||
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 = getBalancesMaxLimit
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"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,26 +14,13 @@ import (
|
||||
type getHoldersRequest struct {
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
getHoldersMaxLimit = 1000
|
||||
getHoldersDefaultLimit = 100
|
||||
)
|
||||
|
||||
func (r getHoldersRequest) Validate() error {
|
||||
var errList []error
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
}
|
||||
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))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
@@ -51,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"`
|
||||
}
|
||||
|
||||
@@ -78,10 +61,6 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getHoldersDefaultLimit
|
||||
}
|
||||
|
||||
var runeId runes.RuneId
|
||||
if req.Id != "" {
|
||||
var ok bool
|
||||
@@ -96,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")
|
||||
}
|
||||
|
||||
@@ -128,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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -83,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)
|
||||
@@ -107,11 +104,8 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
}
|
||||
return errors.Wrap(err, "error during GetTokenInfoByHeight")
|
||||
}
|
||||
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
|
||||
holdingBalances, err := h.usecase.GetBalancesByRuneId(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")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"slices"
|
||||
@@ -16,18 +15,12 @@ import (
|
||||
)
|
||||
|
||||
type getTransactionsRequest struct {
|
||||
Wallet string `query:"wallet"`
|
||||
Id string `query:"id"`
|
||||
FromBlock int64 `query:"fromBlock"`
|
||||
ToBlock int64 `query:"toBlock"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
Wallet string `query:"wallet"`
|
||||
Id string `query:"id"`
|
||||
|
||||
const (
|
||||
getTransactionsMaxLimit = 3000
|
||||
getTransactionsDefaultLimit = 100
|
||||
)
|
||||
FromBlock int64 `query:"fromBlock"`
|
||||
ToBlock int64 `query:"toBlock"`
|
||||
}
|
||||
|
||||
func (r getTransactionsRequest) Validate() error {
|
||||
var errList []error
|
||||
@@ -40,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")
|
||||
}
|
||||
|
||||
@@ -146,9 +133,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
}
|
||||
}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getTransactionsDefaultLimit
|
||||
}
|
||||
|
||||
// default to latest block
|
||||
if req.ToBlock == 0 {
|
||||
@@ -159,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 {
|
||||
@@ -177,11 +158,8 @@ 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")
|
||||
}
|
||||
|
||||
@@ -203,9 +181,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -304,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{
|
||||
|
||||
@@ -2,6 +2,7 @@ package httphandler
|
||||
|
||||
import (
|
||||
"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"
|
||||
@@ -11,20 +12,13 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getUTXOsRequest struct {
|
||||
type getUTXOsByAddressRequest struct {
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Limit int32 `query:"limit"`
|
||||
Offset int32 `query:"offset"`
|
||||
}
|
||||
|
||||
const (
|
||||
getUTXOsMaxLimit = 3000
|
||||
getUTXOsDefaultLimit = 100
|
||||
)
|
||||
|
||||
func (r getUTXOsRequest) Validate() error {
|
||||
func (r getUTXOsByAddressRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
@@ -32,12 +26,6 @@ func (r getUTXOsRequest) Validate() error {
|
||||
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
|
||||
}
|
||||
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))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
@@ -53,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)
|
||||
}
|
||||
@@ -84,60 +71,36 @@ func (h *HttpHandler) GetUTXOs(ctx *fiber.Ctx) (err error) {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = getUTXOsDefaultLimit
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
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,
|
||||
@@ -148,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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -41,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")
|
||||
}()
|
||||
|
||||
@@ -7,14 +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("/holders/:id", h.GetHolders)
|
||||
r.Get("/info/:id", h.GetTokenInfo)
|
||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
|
||||
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
|
||||
r.Get("/utxos/output/:txHash", h.GetUTXOsOutputByLocation)
|
||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOsByAddress)
|
||||
r.Get("/block", h.GetCurrentBlock)
|
||||
return nil
|
||||
}
|
||||
|
||||
27
modules/runes/constants.go
Normal file
27
modules/runes/constants.go
Normal 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")),
|
||||
},
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
)
|
||||
|
||||
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")),
|
||||
},
|
||||
common.NetworkTestnet: {
|
||||
Height: 2519999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("000000000006f45c16402f05d9075db49d3571cf5273cf4cbeaa2aa295f7c833")),
|
||||
},
|
||||
common.NetworkFractalMainnet: {
|
||||
Height: 83999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")), // TODO: Update this to match real hash
|
||||
},
|
||||
common.NetworkFractalTestnet: {
|
||||
Height: 83999,
|
||||
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000000613ddfbdd1778b17cea3818febcbbf82762eafaa9461038343")),
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -118,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;
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
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;
|
||||
@@ -16,28 +16,8 @@ SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_heig
|
||||
-- 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 (
|
||||
@@ -77,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;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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"
|
||||
@@ -28,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.
|
||||
@@ -48,12 +45,10 @@ type RunesReaderDataGateway interface {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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[:])
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"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"
|
||||
@@ -69,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")
|
||||
}
|
||||
}
|
||||
@@ -90,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +121,7 @@ func (p *Processor) ensureValidState(ctx context.Context) error {
|
||||
|
||||
var genesisRuneId = runes.RuneId{BlockHeight: 1, TxIndex: 0}
|
||||
|
||||
func (p *Processor) ensureGenesisRune(ctx context.Context, network common.Network) error {
|
||||
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")
|
||||
@@ -138,8 +137,8 @@ func (p *Processor) ensureGenesisRune(ctx context.Context, network common.Networ
|
||||
Terms: &runes.Terms{
|
||||
Amount: lo.ToPtr(uint128.From64(1)),
|
||||
Cap: &uint128.Max,
|
||||
HeightStart: lo.ToPtr(network.HalvingInterval() * 4),
|
||||
HeightEnd: lo.ToPtr(network.HalvingInterval() * 5),
|
||||
HeightStart: lo.ToPtr(uint64(common.HalvingInterval * 4)),
|
||||
HeightEnd: lo.ToPtr(uint64(common.HalvingInterval * 5)),
|
||||
OffsetStart: nil,
|
||||
OffsetEnd: nil,
|
||||
},
|
||||
@@ -167,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")
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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"
|
||||
@@ -688,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: constants.StartingBlockHeader[p.network].Hash,
|
||||
Height: startingBlockHeader[p.network].Height,
|
||||
Hash: startingBlockHeader[p.network].Hash,
|
||||
EventHash: chainhash.Hash{},
|
||||
CumulativeEventHash: chainhash.Hash{},
|
||||
}
|
||||
@@ -792,9 +791,9 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
|
||||
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,
|
||||
|
||||
@@ -296,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 {
|
||||
@@ -314,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
|
||||
}
|
||||
@@ -347,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 {
|
||||
@@ -365,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
|
||||
}
|
||||
@@ -645,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
|
||||
@@ -791,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,
|
||||
@@ -852,114 +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,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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,
|
||||
&i.Amount,
|
||||
&i.BlockHeight,
|
||||
&i.SpentHeight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -638,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 {
|
||||
|
||||
@@ -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,33 +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 errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errors.WithStack(errs.NotFound)
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -167,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
|
||||
}
|
||||
@@ -324,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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package usecase
|
||||
|
||||
import "github.com/cockroachdb/errors"
|
||||
|
||||
var ErrUTXONotFound = errors.New("utxo not found")
|
||||
@@ -8,18 +8,16 @@ 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")
|
||||
}
|
||||
|
||||
16
modules/runes/usecase/get_outpoint_balances.go
Normal file
16
modules/runes/usecase/get_outpoint_balances.go
Normal 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
|
||||
}
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
"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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,121 +19,3 @@ func VerifySignature(address string, message string, sigBase64 string, defaultNe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SignTxInput(tx *wire.MsgTx, privateKey *btcec.PrivateKey, prevTxOut *wire.TxOut, inputIndex int) (*wire.MsgTx, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
switch address.Type() {
|
||||
case AddressP2TR:
|
||||
witness, err := txscript.TaprootWitnessSignature(
|
||||
tx,
|
||||
sigHashes,
|
||||
inputIndex,
|
||||
prevTxOut.Value,
|
||||
prevTxOut.PkScript,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
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,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
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,
|
||||
txscript.SigHashAll|txscript.SigHashAnyOneCanPay,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,115 +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()
|
||||
|
||||
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)
|
||||
pkScript, err := txscript.PayToAddrScript(address)
|
||||
|
||||
tx, prevTxOut := generateTxAndPrevTxOutFromPkScript(pkScript)
|
||||
signedTx, err := SignTxInput(
|
||||
tx, privKey, prevTxOut, 0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
verifySignedTx(t, signedTx, prevTxOut)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,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()))
|
||||
@@ -91,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
|
||||
|
||||
Reference in New Issue
Block a user