Compare commits

..

29 Commits

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

View File

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

View File

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

View File

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

4
common/bitcoin.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,10 @@ package httphandler
import (
"context"
"fmt"
"slices"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
@@ -17,49 +16,33 @@ type getBalanceQuery struct {
Wallet string `json:"wallet"`
Id string `json:"id"`
BlockHeight uint64 `json:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type getBalancesBatchRequest struct {
type getBalancesByAddressBatchRequest struct {
Queries []getBalanceQuery `json:"queries"`
}
const getBalancesBatchMaxQueries = 100
func (r getBalancesBatchRequest) Validate() error {
func (r getBalancesByAddressBatchRequest) Validate() error {
var errList []error
if len(r.Queries) == 0 {
errList = append(errList, errors.New("at least one query is required"))
}
if len(r.Queries) > getBalancesBatchMaxQueries {
errList = append(errList, errors.Errorf("cannot exceed %d queries", getBalancesBatchMaxQueries))
}
for i, query := range r.Queries {
for _, query := range r.Queries {
if query.Wallet == "" {
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required", i))
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required"))
}
if query.Id != "" && !isRuneIdOrRuneName(query.Id) {
errList = append(errList, errors.Errorf("queries[%d]: 'id' 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,
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@@ -63,18 +62,7 @@ func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64)
return indexedBlock, nil
}
const maxRuneTransactionsLimit = 10000 // temporary limit to prevent large queries from overwhelming the database
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
if limit == -1 {
limit = maxRuneTransactionsLimit
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
if limit > maxRuneTransactionsLimit {
return nil, errors.Wrapf(errs.InvalidArgument, "limit cannot exceed %d", maxRuneTransactionsLimit)
}
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) {
pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript)))
runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String()))
rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{
@@ -89,9 +77,6 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
FromBlock: int32(fromBlock),
ToBlock: int32(toBlock),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
@@ -120,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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,8 @@ package btcutils
import (
"github.com/Cleverse/go-utilities/utils"
verifier "github.com/bitonicnl/verify-signed-message/pkg"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
func VerifySignature(address string, message string, sigBase64 string, defaultNet ...*chaincfg.Params) error {
@@ -23,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
}

View File

@@ -3,14 +3,8 @@ package btcutils
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifySignature(t *testing.T) {
@@ -73,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)
})
}

View File

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