Compare commits

..

51 Commits

Author SHA1 Message Date
Gaze
e4d41cc7a4 feat: skip non-brc20 transfers 2024-06-09 17:00:01 +07:00
Gaze
b45dfd066a fix: remove debug logs 2024-06-09 16:41:58 +07:00
Gaze
2ae5b0835d feat: process brc20 states 2024-06-09 16:29:00 +07:00
Gaze
132dcde715 fix: transfer order 2024-06-09 16:28:42 +07:00
Gaze
4228730a34 fix: inscription logic 2024-06-09 16:17:58 +07:00
Gaze
f3ff5ecb7d fix: bug transfer inscription in same block as inscribe 2024-06-09 15:30:27 +07:00
Gaze
99bdf49f02 feat: brc20 indexing logic 2024-06-09 14:49:13 +07:00
Gaze
806d27fb46 fix: remove wrong incomplete field check 2024-06-08 00:23:51 +07:00
Gaze
7453abec99 fix: remove stop panic 2024-06-07 21:59:02 +07:00
Gaze
0d075c31f8 chore: remove unused util func 2024-06-07 21:48:03 +07:00
Gaze
605ea63167 fix: use decimals for brc20 amounts 2024-06-07 21:37:17 +07:00
Gaze
3fa0a7d975 fix: update migration 2024-06-07 21:20:13 +07:00
Gaze
14142096af feat: update entities for new table columns 2024-06-07 21:17:56 +07:00
Gaze
2bb1bad449 feat: update migrations 2024-06-07 17:27:10 +07:00
Gaze
ccdc4c56ff feat: add brc20 logic 2024-06-07 00:48:31 +07:00
Gaze
f3c6180c17 feat: add more migration and queries 2024-06-06 16:46:25 +07:00
Gaze
ce11033919 fix: use internal query func 2024-06-06 14:04:18 +07:00
Gaze
033dbf7324 fix: inscription transfers primary key add tx_index 2024-06-06 12:55:43 +07:00
Gaze
38c37189fc fix: unique index 2024-06-06 12:09:19 +07:00
Gaze
6d1db50890 fix: rename table 2024-06-05 19:22:12 +07:00
Gaze
0a3800c68a fix: remove transfer count limit 2024-06-05 17:34:49 +07:00
Gaze
cab7d0448e fix: remove pkg errors 2024-06-05 16:12:04 +07:00
Gaze
7c555fe373 fix: get transfers from buffer first 2024-06-05 16:10:54 +07:00
Gaze
a082a35bb6 chore: remove log 2024-05-31 22:52:12 +07:00
Gaze
180ea17284 fix: wrong curse condition 2024-05-31 22:50:30 +07:00
Gaze
fc48ba50b8 fix: remove panic 2024-05-31 20:46:08 +07:00
Gaze
0305762166 fix: incorrect params mapper 2024-05-31 20:44:57 +07:00
Gaze
83b22eb883 feat: implement revert data 2024-05-31 18:49:36 +07:00
Gaze
45f106995a fix: correctly put coinbase at end of block 2024-05-31 14:37:22 +07:00
Gaze
d6c3f90d8c fix: add content to inscription transfers 2024-05-31 12:08:09 +07:00
Gaze
ef575dea85 fix: put coinbase txs at the end of block 2024-05-30 16:03:13 +07:00
Gaze
d70accc80e refactor: move flotsam to entity 2024-05-30 13:08:58 +07:00
Gaze
d8385125c3 fix: insert non-brc20 inscriptions 2024-05-29 18:36:45 +07:00
Gaze
dc44e4bb5c Revert "feat: seed brc20 stats"
This reverts commit 4ae169218f.

# Conflicts:
#	modules/brc20/constants.go
2024-05-29 17:29:05 +07:00
Gaze
f0cb5d651b fix: remove fmt print 2024-05-29 16:28:05 +07:00
Gaze
4ae169218f feat: seed brc20 stats 2024-05-29 16:23:32 +07:00
Gaze
c41da88808 fix: cache outpoint value before returning 2024-05-29 15:40:28 +07:00
Gaze
94b228dd75 fix: check json protocol value 2024-05-29 15:40:20 +07:00
Gaze
518a07e920 fix: change starting block to first ordinals inscription 2024-05-29 15:39:29 +07:00
Gaze
6512f18f94 feat: optimize get outpoint values 2024-05-29 15:19:52 +07:00
Gaze
27161f827d feat: implement migrate 2024-05-29 15:04:05 +07:00
Gaze
db209f68ad fix: optimize processing 2024-05-29 14:43:37 +07:00
Gaze
bb03d439f5 refactor: move processor to main processor 2024-05-28 15:03:51 +07:00
Gaze
71641dd2fb feat: implement brc20 inscription dgs and repos 2024-05-28 14:57:44 +07:00
Gaze
3bb26d012b feat: add inscription processor code 2024-05-27 17:01:56 +07:00
Gaze
bb3c24b472 feat: add base code for brc20 dgs and repos 2024-05-27 14:33:34 +07:00
Gaze
eb65b8dcbc feat: implement envelope tests 2024-05-27 14:33:34 +07:00
Gaze
b3363c7983 feat: implement envelope parsing 2024-05-27 14:33:34 +07:00
Gaze
c8e03e8056 fix: add inscription location table 2024-05-27 14:33:34 +07:00
Gaze
131afac8c2 feat: add base processor code 2024-05-27 14:33:34 +07:00
Gaze
2befd8b124 feat: add brc20 migrations 2024-05-27 14:33:34 +07:00
109 changed files with 7393 additions and 4347 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

@@ -51,6 +51,8 @@ Here is our minimum database disk space requirement for each module.
| ------ | -------------------------- | ---------------------------- |
| Runes | 10 GB | 150 GB |
Here is our minimum database disk space requirement for each module.
#### 4. Prepare `config.yaml` file.
```yaml
@@ -106,7 +108,7 @@ We will be using `docker-compose` for our installation guide. Make sure the `doc
# docker-compose.yaml
services:
gaze-indexer:
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
image: ghcr.io/gaze-network/gaze-indexer:v1.0.0
container_name: gaze-indexer
restart: unless-stopped
ports:

View File

@@ -17,21 +17,16 @@ import (
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/modules/nodesale"
"github.com/gaze-network/indexer-network/modules/brc20"
"github.com/gaze-network/indexer-network/modules/runes"
"github.com/gaze-network/indexer-network/pkg/automaxprocs"
"github.com/gaze-network/indexer-network/pkg/errorhandler"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
"github.com/gaze-network/indexer-network/pkg/middleware/requestlogger"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/favicon"
fiberrecover "github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/samber/do/v2"
"github.com/samber/lo"
"github.com/spf13/cobra"
@@ -40,7 +35,7 @@ import (
// Register Modules
var Modules = do.Package(
do.LazyNamed("runes", runes.New),
do.LazyNamed("nodesale", nodesale.New),
do.LazyNamed("brc20", brc20.New),
)
func NewRunCommand() *cobra.Command {
@@ -142,14 +137,6 @@ func runHandler(cmd *cobra.Command, _ []string) error {
ErrorHandler: errorhandler.NewHTTPErrorHandler(),
})
app.
Use(favicon.New()).
Use(cors.New()).
Use(requestid.New()).
Use(requestcontext.New(
requestcontext.WithRequestId(),
requestcontext.WithClientIP(conf.HTTPServer.RequestIP),
)).
Use(requestlogger.New(conf.HTTPServer.Logger)).
Use(fiberrecover.New(fiberrecover.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {

View File

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

View File

@@ -17,7 +17,7 @@ import (
type migrateDownCmdOptions struct {
DatabaseURL string
Runes bool
Modules string
All bool
}
@@ -59,7 +59,7 @@ func NewMigrateDownCommand() *cobra.Command {
}
flags := cmd.Flags()
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes down migrations")
flags.StringVar(&opts.Modules, "modules", "", "Modules to apply up migrations")
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
flags.BoolVar(&opts.All, "all", false, "Confirm apply ALL down migrations without prompt")
@@ -87,6 +87,8 @@ func migrateDownHandler(opts *migrateDownCmdOptions, _ *cobra.Command, args migr
}
}
modules := strings.Split(opts.Modules, ",")
applyDownMigrations := func(module string, sourcePath string, migrationTable string) error {
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
sourceURL := "file://" + sourcePath
@@ -116,10 +118,15 @@ func migrateDownHandler(opts *migrateDownCmdOptions, _ *cobra.Command, args migr
return nil
}
if opts.Runes {
if lo.Contains(modules, "runes") {
if err := applyDownMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
if lo.Contains(modules, "brc20") {
if err := applyDownMigrations("BRC20", brc20MigrationSource, "brc20_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@@ -11,12 +11,13 @@ import (
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
type migrateUpCmdOptions struct {
DatabaseURL string
Runes bool
Modules string
}
type migrateUpCmdArgs struct {
@@ -54,7 +55,7 @@ func NewMigrateUpCommand() *cobra.Command {
}
flags := cmd.Flags()
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes up migrations")
flags.StringVar(&opts.Modules, "modules", "", "Modules to apply up migrations")
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
return cmd
@@ -72,6 +73,8 @@ func migrateUpHandler(opts *migrateUpCmdOptions, _ *cobra.Command, args migrateU
return errors.Errorf("unsupported database driver: %s", databaseURL.Scheme)
}
modules := strings.Split(opts.Modules, ",")
applyUpMigrations := func(module string, sourcePath string, migrationTable string) error {
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
sourceURL := "file://" + sourcePath
@@ -101,10 +104,15 @@ func migrateUpHandler(opts *migrateUpCmdOptions, _ *cobra.Command, args migrateU
return nil
}
if opts.Runes {
if lo.Contains(modules, "runes") {
if err := applyUpMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
if lo.Contains(modules, "brc20") {
if err := applyUpMigrations("BRC20", brc20MigrationSource, "brc20_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@@ -4,6 +4,7 @@ import "net/url"
const (
runesMigrationSource = "modules/runes/database/postgresql/migrations"
brc20MigrationSource = "modules/brc20/database/postgresql/migrations"
)
func cloneURLWithQuery(u *url.URL, newQuery url.Values) *url.URL {

View File

@@ -23,14 +23,6 @@ reporting:
# HTTP server configuration options.
http_server:
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
logger:
disable: false # disable logger if logger level is `INFO`
request_header: false
request_query: false
requestip: # Client IP extraction configuration options. This is unnecessary if you don't care about the real client IP or if you're not using a reverse proxy.
trusted_proxies_ip: # Cloudflare, GCP Public LB. See: server/internal/middleware/requestcontext/PROXY-IP.md
trusted_proxies_header: # X-Real-IP, CF-Connecting-IP
enable_reject_malformed_request: false # return 403 if request is malformed (invalid IP)
# Meta-protocol modules configuration options.
modules:
@@ -47,10 +39,3 @@ modules:
password: "password"
db_name: "postgres"
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
nodesale:
postgres:
host: "localhost"
port: 5432
user: "postgres"
password: "password"
db_name: "postgres"

View File

@@ -1,5 +1,5 @@
package constants
const (
Version = "v0.2.1"
Version = "v0.0.1"
)

View File

@@ -292,3 +292,19 @@ func (d *BitcoinNodeDatasource) GetBlockHeader(ctx context.Context, height int64
return types.ParseMsgBlockHeader(*block, height), nil
}
// GetTransaction fetch transaction from Bitcoin node
func (d *BitcoinNodeDatasource) GetTransactionOutputs(ctx context.Context, txHash chainhash.Hash) ([]*types.TxOut, error) {
rawTx, err := d.btcclient.GetRawTransaction(&txHash)
if err != nil {
return nil, errors.Wrap(err, "failed to get raw transaction")
}
msgTx := rawTx.MsgTx()
txOuts := make([]*types.TxOut, 0, len(msgTx.TxOut))
for _, txOut := range msgTx.TxOut {
txOuts = append(txOuts, types.ParseTxOut(txOut))
}
return txOuts, nil
}

View File

@@ -91,10 +91,6 @@ func (i *Indexer[T]) Run(ctx context.Context) (err error) {
select {
case <-i.quit:
logger.InfoContext(ctx, "Got quit signal, stopping indexer")
if err := i.Processor.Shutdown(ctx); err != nil {
logger.ErrorContext(ctx, "Failed to shutdown processor", slogx.Error(err))
return errors.Wrap(err, "processor shutdown failed")
}
return nil
case <-ctx.Done():
return nil
@@ -208,9 +204,9 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
}
// validate is input is continuous and no reorg
prevHeader := i.currentBlock
for i, input := range inputs {
header := input.BlockHeader()
for i := 1; i < len(inputs); i++ {
header := inputs[i].BlockHeader()
prevHeader := inputs[i-1].BlockHeader()
if header.Height != prevHeader.Height+1 {
return errors.Wrapf(errs.InternalError, "input is not continuous, input[%d] height: %d, input[%d] height: %d", i-1, prevHeader.Height, i, header.Height)
}
@@ -221,7 +217,6 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
// end current round
return nil
}
prevHeader = header
}
ctx = logger.WithContext(ctx, slog.Int("total_inputs", len(inputs)))

View File

@@ -29,9 +29,6 @@ type Processor[T Input] interface {
// VerifyStates verifies the states of the indexed data and the indexer
// to ensure the last shutdown was graceful and no missing data.
VerifyStates(ctx context.Context) error
// Shutdown gracefully stops the processor. Database connections, network calls, leftover states, etc. should be closed and cleaned up here.
Shutdown(ctx context.Context) error
}
type IndexerWorker interface {

6
go.mod
View File

@@ -5,14 +5,13 @@ go 1.22
require (
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11
github.com/btcsuite/btcd v0.24.0
github.com/btcsuite/btcd/btcec/v2 v2.1.3
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/cockroachdb/errors v1.11.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/gaze-network/uint128 v1.3.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jackc/pgx/v5 v5.5.5
github.com/mcosta74/pgx-slog v0.3.0
github.com/planxnx/concurrent-stream v0.1.5
@@ -26,11 +25,11 @@ require (
github.com/valyala/fasthttp v1.51.0
go.uber.org/automaxprocs v1.5.3
golang.org/x/sync v0.5.0
google.golang.org/protobuf v1.33.0
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
@@ -38,6 +37,7 @@ require (
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsentry/sentry-go v0.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect

6
go.sum
View File

@@ -92,6 +92,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -107,6 +108,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -222,6 +225,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@@ -302,8 +306,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -8,12 +8,10 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
nodesaleconfig "github.com/gaze-network/indexer-network/modules/nodesale/config"
brc20config "github.com/gaze-network/indexer-network/modules/brc20/config"
runesconfig "github.com/gaze-network/indexer-network/modules/runes/config"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
"github.com/gaze-network/indexer-network/pkg/middleware/requestlogger"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/spf13/pflag"
"github.com/spf13/viper"
@@ -62,14 +60,12 @@ type BitcoinNodeClient struct {
}
type Modules struct {
Runes runesconfig.Config `mapstructure:"runes"`
Nodesale nodesaleconfig.Config `mapstructure:"nodesale"`
Runes runesconfig.Config `mapstructure:"runes"`
BRC20 brc20config.Config `mapstructure:"brc20"`
}
type HTTPServerConfig struct {
Port int `mapstructure:"port"`
Logger requestlogger.Config `mapstructure:"logger"`
RequestIP requestcontext.WithClientIPConfig `mapstructure:"requestip"`
Port int `mapstructure:"port"`
}
// Parse parse the configuration from environment variables

71
modules/brc20/brc20.go Normal file
View File

@@ -0,0 +1,71 @@
package brc20
import (
"context"
"strings"
"github.com/btcsuite/btcd/rpcclient"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
brc20postgres "github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres"
"github.com/gaze-network/indexer-network/pkg/btcclient"
"github.com/samber/do/v2"
)
func New(injector do.Injector) (indexer.IndexerWorker, error) {
ctx := do.MustInvoke[context.Context](injector)
conf := do.MustInvoke[config.Config](injector)
// reportingClient := do.MustInvoke[*reportingclient.ReportingClient](injector)
cleanupFuncs := make([]func(context.Context) error, 0)
var brc20Dg datagateway.BRC20DataGateway
var indexerInfoDg datagateway.IndexerInfoDataGateway
switch strings.ToLower(conf.Modules.BRC20.Database) {
case "postgresql", "postgres", "pg":
pg, err := postgres.NewPool(ctx, conf.Modules.BRC20.Postgres)
if err != nil {
if errors.Is(err, errs.InvalidArgument) {
return nil, errors.Wrap(err, "Invalid Postgres configuration for indexer")
}
return nil, errors.Wrap(err, "can't create Postgres connection pool")
}
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
pg.Close()
return nil
})
brc20Repo := brc20postgres.NewRepository(pg)
brc20Dg = brc20Repo
indexerInfoDg = brc20Repo
default:
return nil, errors.Wrapf(errs.Unsupported, "%q database for indexer is not supported", conf.Modules.BRC20.Database)
}
var bitcoinDatasource datasources.Datasource[*types.Block]
var bitcoinClient btcclient.Contract
switch strings.ToLower(conf.Modules.BRC20.Datasource) {
case "bitcoin-node":
btcClient := do.MustInvoke[*rpcclient.Client](injector)
bitcoinNodeDatasource := datasources.NewBitcoinNode(btcClient)
bitcoinDatasource = bitcoinNodeDatasource
bitcoinClient = bitcoinNodeDatasource
default:
return nil, errors.Wrapf(errs.Unsupported, "%q datasource is not supported", conf.Modules.BRC20.Datasource)
}
processor, err := NewProcessor(brc20Dg, indexerInfoDg, bitcoinClient, conf.Network, cleanupFuncs)
if err != nil {
return nil, errors.WithStack(err)
}
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
}
indexer := indexer.New(processor, bitcoinDatasource)
return indexer, nil
}

View File

@@ -0,0 +1,10 @@
package config
import "github.com/gaze-network/indexer-network/internal/postgres"
type Config struct {
Datasource string `mapstructure:"datasource"` // Datasource to fetch bitcoin data for Meta-Protocol e.g. `bitcoin-node`
Database string `mapstructure:"database"` // Database to store data.
APIHandlers []string `mapstructure:"api_handlers"` // List of API handlers to enable. (e.g. `http`)
Postgres postgres.Config `mapstructure:"postgres"`
}

View File

@@ -0,0 +1,25 @@
package brc20
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 (
ClientVersion = "v0.0.1"
DBVersion = 1
EventHashVersion = 1
)
var startingBlockHeader = map[common.Network]types.BlockHeader{
common.NetworkMainnet: {
Height: 767429,
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000000000002b35aef66eb15cd2b232a800f75a2f25cedca4cfe52c4")),
},
common.NetworkTestnet: {
Height: 2413342,
Hash: *utils.Must(chainhash.NewHashFromStr("00000000000022e97030b143af785de812f836dd0651b6ac2b7dd9e90dc9abf9")),
},
}

View File

@@ -0,0 +1,17 @@
BEGIN;
DROP TABLE IF EXISTS "brc20_indexer_states";
DROP TABLE IF EXISTS "brc20_indexed_blocks";
DROP TABLE IF EXISTS "brc20_processor_stats";
DROP TABLE IF EXISTS "brc20_tick_entries";
DROP TABLE IF EXISTS "brc20_tick_entry_states";
DROP TABLE IF EXISTS "brc20_event_deploys";
DROP TABLE IF EXISTS "brc20_event_mints";
DROP TABLE IF EXISTS "brc20_event_inscribe_transfers";
DROP TABLE IF EXISTS "brc20_event_transfer_transfers";
DROP TABLE IF EXISTS "brc20_balances";
DROP TABLE IF EXISTS "brc20_inscription_entries";
DROP TABLE IF EXISTS "brc20_inscription_entry_states";
DROP TABLE IF EXISTS "brc20_inscription_transfers";
COMMIT;

View File

@@ -0,0 +1,189 @@
BEGIN;
-- Indexer Client Information
CREATE TABLE IF NOT EXISTS "brc20_indexer_states" (
"id" BIGSERIAL PRIMARY KEY,
"client_version" TEXT NOT NULL,
"network" TEXT NOT NULL,
"db_version" INT NOT NULL,
"event_hash_version" INT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS brc20_indexer_state_created_at_idx ON "brc20_indexer_states" USING BTREE ("created_at" DESC);
-- BRC20 data
CREATE TABLE IF NOT EXISTS "brc20_indexed_blocks" (
"height" INT NOT NULL PRIMARY KEY,
"hash" TEXT NOT NULL,
"event_hash" TEXT NOT NULL,
"cumulative_event_hash" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_processor_stats" (
"block_height" INT NOT NULL PRIMARY KEY,
"cursed_inscription_count" INT NOT NULL,
"blessed_inscription_count" INT NOT NULL,
"lost_sats" BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_tick_entries" (
"tick" TEXT NOT NULL PRIMARY KEY, -- lowercase of original_tick
"original_tick" TEXT NOT NULL,
"total_supply" DECIMAL NOT NULL,
"decimals" SMALLINT NOT NULL,
"limit_per_mint" DECIMAL NOT NULL,
"is_self_mint" BOOLEAN NOT NULL,
"deploy_inscription_id" TEXT NOT NULL,
"deployed_at" TIMESTAMP NOT NULL,
"deployed_at_height" INT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_tick_entry_states" (
"tick" TEXT NOT NULL,
"block_height" INT NOT NULL,
"minted_amount" DECIMAL NOT NULL,
"burned_amount" DECIMAL NOT NULL,
"completed_at" TIMESTAMP,
"completed_at_height" INT,
PRIMARY KEY ("tick", "block_height")
);
CREATE TABLE IF NOT EXISTS "brc20_event_deploys" (
"id" BIGINT PRIMARY KEY NOT NULL,
"inscription_id" TEXT NOT NULL,
"inscription_number" BIGINT NOT NULL,
"tick" TEXT NOT NULL, -- lowercase of original_tick
"original_tick" TEXT NOT NULL,
"tx_hash" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tx_index" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL,
"pkscript" TEXT NOT NULL,
"satpoint" TEXT NOT NULL,
"total_supply" DECIMAL NOT NULL,
"decimals" SMALLINT NOT NULL,
"limit_per_mint" DECIMAL NOT NULL,
"is_self_mint" BOOLEAN NOT NULL
);
CREATE INDEX IF NOT EXISTS brc20_event_deploys_block_height_idx ON "brc20_event_deploys" USING BTREE ("block_height");
CREATE TABLE IF NOT EXISTS "brc20_event_mints" (
"id" BIGINT PRIMARY KEY NOT NULL,
"inscription_id" TEXT NOT NULL,
"inscription_number" BIGINT NOT NULL,
"tick" TEXT NOT NULL, -- lowercase of original_tick
"original_tick" TEXT NOT NULL,
"tx_hash" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tx_index" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL,
"pkscript" TEXT NOT NULL,
"satpoint" TEXT NOT NULL,
"amount" DECIMAL NOT NULL,
"parent_id" TEXT -- requires parent deploy inscription id if minting a self-mint ticker
);
CREATE INDEX IF NOT EXISTS brc20_event_mints_block_height_idx ON "brc20_event_mints" USING BTREE ("block_height");
CREATE TABLE IF NOT EXISTS "brc20_event_inscribe_transfers" (
"id" BIGINT PRIMARY KEY NOT NULL,
"inscription_id" TEXT NOT NULL,
"inscription_number" BIGINT NOT NULL,
"tick" TEXT NOT NULL, -- lowercase of original_tick
"original_tick" TEXT NOT NULL,
"tx_hash" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tx_index" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL,
"pkscript" TEXT NOT NULL,
"satpoint" TEXT NOT NULL,
"output_index" INT NOT NULL,
"sats_amount" BIGINT NOT NULL,
"amount" DECIMAL NOT NULL
);
CREATE INDEX IF NOT EXISTS brc20_event_inscribe_transfers_block_height_idx ON "brc20_event_inscribe_transfers" USING BTREE ("block_height");
CREATE INDEX IF NOT EXISTS brc20_event_inscribe_transfers_inscription_id_idx ON "brc20_event_inscribe_transfers" USING BTREE ("inscription_id"); -- used for validating transfer transfer events
CREATE TABLE IF NOT EXISTS "brc20_event_transfer_transfers" (
"id" BIGINT PRIMARY KEY NOT NULL,
"inscription_id" TEXT NOT NULL,
"inscription_number" BIGINT NOT NULL,
"tick" TEXT NOT NULL, -- lowercase of original_tick
"original_tick" TEXT NOT NULL,
"tx_hash" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tx_index" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL,
"from_pkscript" TEXT NOT NULL,
"from_satpoint" TEXT NOT NULL,
"from_input_index" INT NOT NULL,
"to_pkscript" TEXT NOT NULL,
"to_satpoint" TEXT NOT NULL,
"to_output_index" INT NOT NULL,
"spent_as_fee" BOOLEAN NOT NULL,
"amount" DECIMAL NOT NULL
);
CREATE INDEX IF NOT EXISTS brc20_event_transfer_transfers_block_height_idx ON "brc20_event_transfer_transfers" USING BTREE ("block_height");
CREATE TABLE IF NOT EXISTS "brc20_balances" (
"pkscript" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tick" TEXT NOT NULL,
"overall_balance" DECIMAL NOT NULL,
"available_balance" DECIMAL NOT NULL,
PRIMARY KEY ("pkscript", "tick", "block_height")
);
CREATE TABLE IF NOT EXISTS "brc20_inscription_entries" (
"id" TEXT NOT NULL PRIMARY KEY,
"number" BIGINT NOT NULL,
"sequence_number" BIGINT NOT NULL,
"delegate" TEXT, -- delegate inscription id
"metadata" BYTEA,
"metaprotocol" TEXT,
"parents" TEXT[], -- parent inscription id, 0.14 only supports 1 parent per inscription
"pointer" BIGINT,
"content" JSONB, -- can use jsonb because we only track brc20 inscriptions
"content_encoding" TEXT,
"content_type" TEXT,
"cursed" BOOLEAN NOT NULL, -- inscriptions after jubilee are no longer cursed in 0.14, which affects inscription number
"cursed_for_brc20" BOOLEAN NOT NULL, -- however, inscriptions that would normally be cursed are still considered cursed for brc20
"created_at" TIMESTAMP NOT NULL,
"created_at_height" INT NOT NULL
);
CREATE INDEX IF NOT EXISTS brc20_inscription_entries_id_number_idx ON "brc20_inscription_entries" USING BTREE ("id", "number");
CREATE TABLE IF NOT EXISTS "brc20_inscription_entry_states" (
"id" TEXT NOT NULL,
"block_height" INT NOT NULL,
"transfer_count" INT NOT NULL,
PRIMARY KEY ("id", "block_height")
);
CREATE TABLE IF NOT EXISTS "brc20_inscription_transfers" (
"inscription_id" TEXT NOT NULL,
"block_height" INT NOT NULL,
"tx_index" INT NOT NULL,
"tx_hash" TEXT NOT NULL,
"from_input_index" INT NOT NULL,
"old_satpoint_tx_hash" TEXT,
"old_satpoint_out_idx" INT,
"old_satpoint_offset" BIGINT,
"new_satpoint_tx_hash" TEXT,
"new_satpoint_out_idx" INT,
"new_satpoint_offset" BIGINT,
"new_pkscript" TEXT NOT NULL,
"new_output_value" BIGINT NOT NULL,
"sent_as_fee" BOOLEAN NOT NULL,
"transfer_count" INT NOT NULL,
PRIMARY KEY ("inscription_id", "block_height", "tx_index")
);
CREATE INDEX IF NOT EXISTS brc20_inscription_transfers_block_height_tx_index_idx ON "brc20_inscription_transfers" USING BTREE ("block_height", "tx_index");
CREATE INDEX IF NOT EXISTS brc20_inscription_transfers_new_satpoint_idx ON "brc20_inscription_transfers" USING BTREE ("new_satpoint_tx_hash", "new_satpoint_out_idx", "new_satpoint_offset");
COMMIT;

View File

@@ -0,0 +1,142 @@
-- name: GetLatestIndexedBlock :one
SELECT * FROM "brc20_indexed_blocks" ORDER BY "height" DESC LIMIT 1;
-- name: GetIndexedBlockByHeight :one
SELECT * FROM "brc20_indexed_blocks" WHERE "height" = $1;
-- name: GetLatestProcessorStats :one
SELECT * FROM "brc20_processor_stats" ORDER BY "block_height" DESC LIMIT 1;
-- name: GetInscriptionTransfersInOutPoints :many
SELECT "it".*, "ie"."content" FROM (
SELECT
unnest(@tx_hash_arr::text[]) AS "tx_hash",
unnest(@tx_out_idx_arr::int[]) AS "tx_out_idx"
) "inputs"
INNER JOIN "brc20_inscription_transfers" it ON "inputs"."tx_hash" = "it"."new_satpoint_tx_hash" AND "inputs"."tx_out_idx" = "it"."new_satpoint_out_idx"
LEFT JOIN "brc20_inscription_entries" ie ON "it"."inscription_id" = "ie"."id";
;
-- name: GetInscriptionEntriesByIds :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("id") * FROM "brc20_inscription_entry_states" WHERE "id" = ANY(@inscription_ids::text[]) ORDER BY "id", "block_height" DESC
)
SELECT * FROM "brc20_inscription_entries"
LEFT JOIN "states" ON "brc20_inscription_entries"."id" = "states"."id"
WHERE "brc20_inscription_entries"."id" = ANY(@inscription_ids::text[]);
-- name: GetTickEntriesByTicks :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("tick") * FROM "brc20_tick_entry_states" WHERE "tick" = ANY(@ticks::text[]) ORDER BY "tick", "block_height" DESC
)
SELECT * FROM "brc20_tick_entries"
LEFT JOIN "states" ON "brc20_tick_entries"."tick" = "states"."tick"
WHERE "brc20_tick_entries"."tick" = ANY(@ticks::text[]);
-- name: GetInscriptionNumbersByIds :many
SELECT id, number FROM "brc20_inscription_entries" WHERE "id" = ANY(@inscription_ids::text[]);
-- name: GetInscriptionParentsByIds :many
SELECT id, parents FROM "brc20_inscription_entries" WHERE "id" = ANY(@inscription_ids::text[]);
-- name: GetLatestEventIds :one
WITH "latest_deploy_id" AS (
SELECT "id" FROM "brc20_event_deploys" ORDER BY "id" DESC LIMIT 1
),
"latest_mint_id" AS (
SELECT "id" FROM "brc20_event_mints" ORDER BY "id" DESC LIMIT 1
),
"latest_inscribe_transfer_id" AS (
SELECT "id" FROM "brc20_event_inscribe_transfers" ORDER BY "id" DESC LIMIT 1
),
"latest_transfer_transfer_id" AS (
SELECT "id" FROM "brc20_event_transfer_transfers" ORDER BY "id" DESC LIMIT 1
)
SELECT
COALESCE((SELECT "id" FROM "latest_deploy_id"), -1) AS "event_deploy_id",
COALESCE((SELECT "id" FROM "latest_mint_id"), -1) AS "event_mint_id",
COALESCE((SELECT "id" FROM "latest_inscribe_transfer_id"), -1) AS "event_inscribe_transfer_id",
COALESCE((SELECT "id" FROM "latest_transfer_transfer_id"), -1) AS "event_transfer_transfer_id";
-- name: GetBalancesBatchAtHeight :many
SELECT DISTINCT ON ("brc20_balances"."pkscript", "brc20_balances"."tick") "brc20_balances".* FROM "brc20_balances"
INNER JOIN (
SELECT
unnest(@pkscript_arr::text[]) AS "pkscript",
unnest(@tick_arr::text[]) AS "tick"
) "queries" ON "brc20_balances"."pkscript" = "queries"."pkscript" AND "brc20_balances"."tick" = "queries"."tick" AND "brc20_balances"."block_height" <= @block_height
ORDER BY "brc20_balances"."pkscript", "brc20_balances"."tick", "block_height" DESC;
-- name: GetEventInscribeTransfersByInscriptionIds :many
SELECT * FROM "brc20_event_inscribe_transfers" WHERE "inscription_id" = ANY(@inscription_ids::text[]);
-- name: CreateIndexedBlock :exec
INSERT INTO "brc20_indexed_blocks" ("height", "hash", "event_hash", "cumulative_event_hash") VALUES ($1, $2, $3, $4);
-- name: CreateProcessorStats :exec
INSERT INTO "brc20_processor_stats" ("block_height", "cursed_inscription_count", "blessed_inscription_count", "lost_sats") VALUES ($1, $2, $3, $4);
-- name: CreateTickEntries :batchexec
INSERT INTO "brc20_tick_entries" ("tick", "original_tick", "total_supply", "decimals", "limit_per_mint", "is_self_mint", "deploy_inscription_id", "deployed_at", "deployed_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
-- name: CreateTickEntryStates :batchexec
INSERT INTO "brc20_tick_entry_states" ("tick", "block_height", "minted_amount", "burned_amount", "completed_at", "completed_at_height") VALUES ($1, $2, $3, $4, $5, $6);
-- name: CreateInscriptionEntries :batchexec
INSERT INTO "brc20_inscription_entries" ("id", "number", "sequence_number", "delegate", "metadata", "metaprotocol", "parents", "pointer", "content", "content_encoding", "content_type", "cursed", "cursed_for_brc20", "created_at", "created_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15);
-- name: CreateInscriptionEntryStates :batchexec
INSERT INTO "brc20_inscription_entry_states" ("id", "block_height", "transfer_count") VALUES ($1, $2, $3);
-- name: CreateInscriptionTransfers :batchexec
INSERT INTO "brc20_inscription_transfers" ("inscription_id", "block_height", "tx_index", "tx_hash", "from_input_index", "old_satpoint_tx_hash", "old_satpoint_out_idx", "old_satpoint_offset", "new_satpoint_tx_hash", "new_satpoint_out_idx", "new_satpoint_offset", "new_pkscript", "new_output_value", "sent_as_fee", "transfer_count") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15);
-- name: CreateEventDeploys :batchexec
INSERT INTO "brc20_event_deploys" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "total_supply", "decimals", "limit_per_mint", "is_self_mint") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);
-- name: CreateEventMints :batchexec
INSERT INTO "brc20_event_mints" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "amount", "parent_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
-- name: CreateEventInscribeTransfers :batchexec
INSERT INTO "brc20_event_inscribe_transfers" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "output_index", "sats_amount", "amount") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);
-- name: CreateEventTransferTransfers :batchexec
INSERT INTO "brc20_event_transfer_transfers" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "from_pkscript", "from_satpoint", "from_input_index", "to_pkscript", "to_satpoint", "to_output_index", "spent_as_fee", "amount") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16);
-- name: DeleteIndexedBlocksSinceHeight :exec
DELETE FROM "brc20_indexed_blocks" WHERE "height" >= $1;
-- name: DeleteProcessorStatsSinceHeight :exec
DELETE FROM "brc20_processor_stats" WHERE "block_height" >= $1;
-- name: DeleteTickEntriesSinceHeight :exec
DELETE FROM "brc20_tick_entries" WHERE "deployed_at_height" >= $1;
-- name: DeleteTickEntryStatesSinceHeight :exec
DELETE FROM "brc20_tick_entry_states" WHERE "block_height" >= $1;
-- name: DeleteEventDeploysSinceHeight :exec
DELETE FROM "brc20_event_deploys" WHERE "block_height" >= $1;
-- name: DeleteEventMintsSinceHeight :exec
DELETE FROM "brc20_event_mints" WHERE "block_height" >= $1;
-- name: DeleteEventInscribeTransfersSinceHeight :exec
DELETE FROM "brc20_event_inscribe_transfers" WHERE "block_height" >= $1;
-- name: DeleteEventTransferTransfersSinceHeight :exec
DELETE FROM "brc20_event_transfer_transfers" WHERE "block_height" >= $1;
-- name: DeleteBalancesSinceHeight :exec
DELETE FROM "brc20_balances" WHERE "block_height" >= $1;
-- name: DeleteInscriptionEntriesSinceHeight :exec
DELETE FROM "brc20_inscription_entries" WHERE "created_at_height" >= $1;
-- name: DeleteInscriptionEntryStatesSinceHeight :exec
DELETE FROM "brc20_inscription_entry_states" WHERE "block_height" >= $1;
-- name: DeleteInscriptionTransfersSinceHeight :exec
DELETE FROM "brc20_inscription_transfers" WHERE "block_height" >= $1;

View File

@@ -0,0 +1,5 @@
-- name: GetLatestIndexerState :one
SELECT * FROM brc20_indexer_states ORDER BY created_at DESC LIMIT 1;
-- name: CreateIndexerState :exec
INSERT INTO brc20_indexer_states (client_version, network, db_version, event_hash_version) VALUES ($1, $2, $3, $4);

View File

@@ -0,0 +1,16 @@
package brc20
import "github.com/gaze-network/indexer-network/common"
var selfMintActivationHeights = map[common.Network]uint64{
common.NetworkMainnet: 837090,
common.NetworkTestnet: 837090,
}
func isSelfMintActivated(height uint64, network common.Network) bool {
activationHeight, ok := selfMintActivationHeights[network]
if !ok {
return false
}
return height >= activationHeight
}

View File

@@ -0,0 +1,21 @@
package brc20
type Operation string
const (
OperationDeploy Operation = "deploy"
OperationMint Operation = "mint"
OperationTransfer Operation = "transfer"
)
func (o Operation) IsValid() bool {
switch o {
case OperationDeploy, OperationMint, OperationTransfer:
return true
}
return false
}
func (o Operation) String() string {
return string(o)
}

View File

@@ -0,0 +1,170 @@
package brc20
import (
"encoding/json"
"math"
"math/big"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/shopspring/decimal"
)
type rawPayload struct {
P string // required
Op string `json:"op"` // required
Tick string `json:"tick"` // required
// for deploy operations
Max string `json:"max"` // required
Lim *string `json:"lim"`
Dec *string `json:"dec"`
SelfMint *string `json:"self_mint"`
// for mint/transfer operations
Amt string `json:"amt"` // required
}
type Payload struct {
Transfer *entity.InscriptionTransfer
P string
Op Operation
Tick string // lower-cased tick
OriginalTick string // original tick before lower-cased
// for deploy operations
Max decimal.Decimal
Lim decimal.Decimal
Dec uint16
SelfMint bool
// for mint/transfer operations
Amt decimal.Decimal
}
var (
ErrInvalidProtocol = errors.New("invalid protocol: must be 'brc20'")
ErrInvalidOperation = errors.New("invalid operation for brc20: must be one of 'deploy', 'mint', or 'transfer'")
ErrInvalidTickLength = errors.New("invalid tick length: must be 4 or 5 bytes")
ErrEmptyTick = errors.New("empty tick")
ErrEmptyMax = errors.New("empty max")
ErrInvalidMax = errors.New("invalid max")
ErrInvalidDec = errors.New("invalid dec")
ErrInvalidSelfMint = errors.New("invalid self_mint")
ErrInvalidAmt = errors.New("invalid amt")
ErrNumberOverflow = errors.New("number overflow: max value is (2^64-1) * 10^18")
)
func ParsePayload(transfer *entity.InscriptionTransfer) (*Payload, error) {
var p rawPayload
err := json.Unmarshal(transfer.Content, &p)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal payload as json")
}
if p.P != "brc20" {
return nil, errors.WithStack(ErrInvalidProtocol)
}
if !Operation(p.Op).IsValid() {
return nil, errors.WithStack(ErrInvalidOperation)
}
if p.Tick == "" {
return nil, errors.WithStack(ErrEmptyTick)
}
if len(p.Tick) != 4 && len(p.Tick) != 5 {
return nil, errors.WithStack(ErrInvalidTickLength)
}
parsed := Payload{
Transfer: transfer,
P: p.P,
Op: Operation(p.Op),
Tick: strings.ToLower(p.Tick),
OriginalTick: p.Tick,
}
switch parsed.Op {
case OperationDeploy:
if p.Max == "" {
return nil, errors.WithStack(ErrEmptyMax)
}
var rawDec string
if p.Dec != nil {
rawDec = *p.Dec
}
dec, ok := strconv.ParseUint(rawDec, 10, 16)
if ok != nil {
return nil, errors.Wrap(ok, "failed to parse dec")
}
if dec > 18 {
return nil, errors.WithStack(ErrInvalidDec)
}
parsed.Dec = uint16(dec)
max, err := parseNumericString(p.Max, dec)
if err != nil {
return nil, errors.Wrap(err, "failed to parse max")
}
parsed.Max = max
limit := max
if p.Lim != nil {
limit, err = parseNumericString(*p.Lim, dec)
if err != nil {
return nil, errors.Wrap(err, "failed to parse lim")
}
}
parsed.Lim = limit
// 5-bytes ticks are self-mint only
if len(parsed.OriginalTick) == 5 {
if p.SelfMint == nil || *p.SelfMint != "true" {
return nil, errors.WithStack(ErrInvalidSelfMint)
}
// infinite mints if tick is self-mint, and max is set to 0
if parsed.Max.IsZero() {
parsed.Max = maxNumber
if parsed.Lim.IsZero() {
parsed.Lim = maxNumber
}
}
}
if parsed.Max.IsZero() {
return nil, errors.WithStack(ErrInvalidMax)
}
case OperationMint, OperationTransfer:
if p.Amt == "" {
return nil, errors.WithStack(ErrInvalidAmt)
}
// NOTE: check tick decimals after parsing payload
amt, err := parseNumericString(p.Amt, 18)
if err != nil {
return nil, errors.Wrap(err, "failed to parse amt")
}
parsed.Amt = amt
default:
return nil, errors.WithStack(ErrInvalidOperation)
}
return &parsed, nil
}
// max number for all numeric fields (except dec) is (2^64-1)
var (
maxNumber = decimal.NewFromBigInt(new(big.Int).SetUint64(math.MaxUint64), 0)
)
func parseNumericString(s string, maxDec uint64) (decimal.Decimal, error) {
d, err := decimal.NewFromString(s)
if err != nil {
return decimal.Decimal{}, errors.Wrap(err, "failed to parse decimal number")
}
if -d.Exponent() > int32(maxDec) {
return decimal.Decimal{}, errors.Errorf("cannot parse decimal number: too many decimal points: expected %d got %d", maxDec, d.Exponent())
}
if d.GreaterThan(maxNumber) {
return decimal.Decimal{}, errors.WithStack(ErrNumberOverflow)
}
return d, nil
}

View File

@@ -0,0 +1,71 @@
package datagateway
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
)
type BRC20DataGateway interface {
BRC20ReaderDataGateway
BRC20WriterDataGateway
// BeginBRC20Tx returns a new BRC20DataGateway with transaction enabled. All write operations performed in this datagateway must be committed to persist changes.
BeginBRC20Tx(ctx context.Context) (BRC20DataGatewayWithTx, error)
}
type BRC20DataGatewayWithTx interface {
BRC20DataGateway
Tx
}
type BRC20ReaderDataGateway interface {
GetLatestBlock(ctx context.Context) (types.BlockHeader, error)
GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error)
GetProcessorStats(ctx context.Context) (*entity.ProcessorStats, error)
GetInscriptionTransfersInOutPoints(ctx context.Context, outPoints []wire.OutPoint) (map[ordinals.SatPoint][]*entity.InscriptionTransfer, error)
GetInscriptionEntriesByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*ordinals.InscriptionEntry, error)
GetInscriptionNumbersByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]int64, error)
GetInscriptionParentsByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]ordinals.InscriptionId, error)
GetBalancesBatchAtHeight(ctx context.Context, blockHeight uint64, queries []GetBalancesBatchAtHeightQuery) (map[string]map[string]*entity.Balance, error)
GetTickEntriesByTicks(ctx context.Context, ticks []string) (map[string]*entity.TickEntry, error)
GetEventInscribeTransfersByInscriptionIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*entity.EventInscribeTransfer, error)
GetLatestEventId(ctx context.Context) (int64, error)
}
type BRC20WriterDataGateway interface {
CreateIndexedBlock(ctx context.Context, block *entity.IndexedBlock) error
CreateProcessorStats(ctx context.Context, stats *entity.ProcessorStats) error
CreateTickEntries(ctx context.Context, blockHeight uint64, entries []*entity.TickEntry) error
CreateTickEntryStates(ctx context.Context, blockHeight uint64, entryStates []*entity.TickEntry) error
CreateInscriptionEntries(ctx context.Context, blockHeight uint64, entries []*ordinals.InscriptionEntry) error
CreateInscriptionEntryStates(ctx context.Context, blockHeight uint64, entryStates []*ordinals.InscriptionEntry) error
CreateInscriptionTransfers(ctx context.Context, transfers []*entity.InscriptionTransfer) error
CreateEventDeploys(ctx context.Context, events []*entity.EventDeploy) error
CreateEventMints(ctx context.Context, events []*entity.EventMint) error
CreateEventInscribeTransfers(ctx context.Context, events []*entity.EventInscribeTransfer) error
CreateEventTransferTransfers(ctx context.Context, events []*entity.EventTransferTransfer) error
// used for revert data
DeleteIndexedBlocksSinceHeight(ctx context.Context, height uint64) error
DeleteProcessorStatsSinceHeight(ctx context.Context, height uint64) error
DeleteTickEntriesSinceHeight(ctx context.Context, height uint64) error
DeleteTickEntryStatesSinceHeight(ctx context.Context, height uint64) error
DeleteEventDeploysSinceHeight(ctx context.Context, height uint64) error
DeleteEventMintsSinceHeight(ctx context.Context, height uint64) error
DeleteEventInscribeTransfersSinceHeight(ctx context.Context, height uint64) error
DeleteEventTransferTransfersSinceHeight(ctx context.Context, height uint64) error
DeleteBalancesSinceHeight(ctx context.Context, height uint64) error
DeleteInscriptionEntriesSinceHeight(ctx context.Context, height uint64) error
DeleteInscriptionEntryStatesSinceHeight(ctx context.Context, height uint64) error
DeleteInscriptionTransfersSinceHeight(ctx context.Context, height uint64) error
}
type GetBalancesBatchAtHeightQuery struct {
PkScriptHex string
Tick string
BlockHeight uint64
}

View File

@@ -0,0 +1,12 @@
package datagateway
import (
"context"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
)
type IndexerInfoDataGateway interface {
GetLatestIndexerState(ctx context.Context) (entity.IndexerState, error)
CreateIndexerState(ctx context.Context, state entity.IndexerState) error
}

View File

@@ -0,0 +1,12 @@
package datagateway
import "context"
type Tx interface {
// Commit commits the DB transaction. All changes made after Begin() will be persisted. Calling Commit() will close the current transaction.
// If Commit() is called without a prior Begin(), it must be a no-op.
Commit(ctx context.Context) error
// Rollback rolls back the DB transaction. All changes made after Begin() will be discarded.
// Rollback() must be safe to call even if no transaction is active. Hence, a defer Rollback() is safe, even if Commit() was called prior with non-error conditions.
Rollback(ctx context.Context) error
}

View File

@@ -0,0 +1,11 @@
package entity
import "github.com/shopspring/decimal"
type Balance struct {
PkScript []byte
Tick string
BlockHeight uint64
OverallBalance decimal.Decimal
AvailableBalance decimal.Decimal
}

View File

@@ -0,0 +1,28 @@
package entity
import (
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/shopspring/decimal"
)
type EventDeploy struct {
Id int64
InscriptionId ordinals.InscriptionId
InscriptionNumber int64
Tick string
OriginalTick string
TxHash chainhash.Hash
BlockHeight uint64
TxIndex uint32
Timestamp time.Time
PkScript []byte
SatPoint ordinals.SatPoint
TotalSupply decimal.Decimal
Decimals uint16
LimitPerMint decimal.Decimal
IsSelfMint bool
}

View File

@@ -0,0 +1,27 @@
package entity
import (
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/shopspring/decimal"
)
type EventInscribeTransfer struct {
Id int64
InscriptionId ordinals.InscriptionId
InscriptionNumber int64
Tick string
OriginalTick string
TxHash chainhash.Hash
BlockHeight uint64
TxIndex uint32
Timestamp time.Time
PkScript []byte
SatPoint ordinals.SatPoint
OutputIndex uint32
SatsAmount uint64
Amount decimal.Decimal
}

View File

@@ -0,0 +1,26 @@
package entity
import (
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/shopspring/decimal"
)
type EventMint struct {
Id int64
InscriptionId ordinals.InscriptionId
InscriptionNumber int64
Tick string
OriginalTick string
TxHash chainhash.Hash
BlockHeight uint64
TxIndex uint32
Timestamp time.Time
PkScript []byte
SatPoint ordinals.SatPoint
Amount decimal.Decimal
ParentId *ordinals.InscriptionId
}

View File

@@ -0,0 +1,30 @@
package entity
import (
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/shopspring/decimal"
)
type EventTransferTransfer struct {
Id int64
InscriptionId ordinals.InscriptionId
InscriptionNumber int64
Tick string
OriginalTick string
TxHash chainhash.Hash
BlockHeight uint64
TxIndex uint32
Timestamp time.Time
FromPkScript []byte
FromSatPoint ordinals.SatPoint
FromInputIndex uint32
ToPkScript []byte
ToSatPoint ordinals.SatPoint
ToOutputIndex uint32
SpentAsFee bool
Amount decimal.Decimal
}

View File

@@ -0,0 +1,31 @@
package entity
import (
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
)
type OriginOld struct {
Content []byte
OldSatPoint ordinals.SatPoint
InputIndex uint32
}
type OriginNew struct {
Inscription ordinals.Inscription
Parent *ordinals.InscriptionId
Pointer *uint64
Fee uint64
Cursed bool
CursedForBRC20 bool
Hidden bool
Reinscription bool
Unbound bool
}
type Flotsam struct {
Tx *types.Transaction
OriginOld *OriginOld // OriginOld and OriginNew are mutually exclusive
OriginNew *OriginNew // OriginOld and OriginNew are mutually exclusive
Offset uint64
InscriptionId ordinals.InscriptionId
}

View File

@@ -0,0 +1,10 @@
package entity
import "github.com/btcsuite/btcd/chaincfg/chainhash"
type IndexedBlock struct {
Height uint64
Hash chainhash.Hash
EventHash chainhash.Hash
CumulativeEventHash chainhash.Hash
}

View File

@@ -0,0 +1,15 @@
package entity
import (
"time"
"github.com/gaze-network/indexer-network/common"
)
type IndexerState struct {
CreatedAt time.Time
ClientVersion string
DBVersion int32
EventHashVersion int32
Network common.Network
}

View File

@@ -0,0 +1,21 @@
package entity
import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
)
type InscriptionTransfer struct {
InscriptionId ordinals.InscriptionId
BlockHeight uint64
TxIndex uint32
TxHash chainhash.Hash
Content []byte
FromInputIndex uint32
OldSatPoint ordinals.SatPoint
NewSatPoint ordinals.SatPoint
NewPkScript []byte
NewOutputValue uint64
SentAsFee bool
TransferCount uint32
}

View File

@@ -0,0 +1,8 @@
package entity
type ProcessorStats struct {
BlockHeight uint64
CursedInscriptionCount uint64
BlessedInscriptionCount uint64
LostSats uint64
}

View File

@@ -0,0 +1,25 @@
package entity
import (
"time"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/shopspring/decimal"
)
type TickEntry struct {
Tick string
OriginalTick string
TotalSupply decimal.Decimal
Decimals uint16
LimitPerMint decimal.Decimal
IsSelfMint bool
DeployInscriptionId ordinals.InscriptionId
DeployedAt time.Time
DeployedAtHeight uint64
MintedAmount decimal.Decimal
BurnedAmount decimal.Decimal
CompletedAt time.Time
CompletedAtHeight uint64
}

View File

@@ -0,0 +1,285 @@
package ordinals
import (
"bytes"
"encoding/binary"
"github.com/btcsuite/btcd/txscript"
"github.com/gaze-network/indexer-network/core/types"
"github.com/samber/lo"
)
type Envelope struct {
Inscription Inscription
InputIndex uint32 // Index of input that contains the envelope
Offset int // Number of envelope in the input
PushNum bool // True if envelope contains pushnum opcodes
Stutter bool // True if envelope matches stuttering curse structure
IncompleteField bool // True if payload is incomplete
DuplicateField bool // True if payload contains duplicated field
UnrecognizedEvenField bool // True if payload contains unrecognized even field
}
func ParseEnvelopesFromTx(tx *types.Transaction) []*Envelope {
envelopes := make([]*Envelope, 0)
for i, txIn := range tx.TxIn {
tapScript, ok := extractTapScript(txIn.Witness)
if !ok {
continue
}
newEnvelopes := envelopesFromTapScript(tapScript, i)
envelopes = append(envelopes, newEnvelopes...)
}
return envelopes
}
var protocolId = []byte("ord")
func envelopesFromTapScript(tokenizer txscript.ScriptTokenizer, inputIndex int) []*Envelope {
envelopes := make([]*Envelope, 0)
var stuttered bool
for tokenizer.Next() {
if tokenizer.Err() != nil {
break
}
if tokenizer.Opcode() == txscript.OP_FALSE {
envelope, stutter := envelopeFromTokenizer(tokenizer, inputIndex, len(envelopes), stuttered)
if envelope != nil {
envelopes = append(envelopes, envelope)
} else {
stuttered = stutter
}
}
}
if tokenizer.Err() != nil {
return envelopes
}
return envelopes
}
func envelopeFromTokenizer(tokenizer txscript.ScriptTokenizer, inputIndex int, offset int, stuttered bool) (*Envelope, bool) {
tokenizer.Next()
if tokenizer.Opcode() != txscript.OP_IF {
return nil, tokenizer.Opcode() == txscript.OP_FALSE
}
tokenizer.Next()
if !bytes.Equal(tokenizer.Data(), protocolId) {
return nil, tokenizer.Opcode() == txscript.OP_FALSE
}
var pushNum bool
payload := make([][]byte, 0)
for tokenizer.Next() {
if tokenizer.Err() != nil {
return nil, false
}
opCode := tokenizer.Opcode()
if opCode == txscript.OP_ENDIF {
break
}
switch opCode {
case txscript.OP_1NEGATE:
pushNum = true
payload = append(payload, []byte{0x81})
case txscript.OP_1:
pushNum = true
payload = append(payload, []byte{0x01})
case txscript.OP_2:
pushNum = true
payload = append(payload, []byte{0x02})
case txscript.OP_3:
pushNum = true
payload = append(payload, []byte{0x03})
case txscript.OP_4:
pushNum = true
payload = append(payload, []byte{0x04})
case txscript.OP_5:
pushNum = true
payload = append(payload, []byte{0x05})
case txscript.OP_6:
pushNum = true
payload = append(payload, []byte{0x06})
case txscript.OP_7:
pushNum = true
payload = append(payload, []byte{0x07})
case txscript.OP_8:
pushNum = true
payload = append(payload, []byte{0x08})
case txscript.OP_9:
pushNum = true
payload = append(payload, []byte{0x09})
case txscript.OP_10:
pushNum = true
payload = append(payload, []byte{0x10})
case txscript.OP_11:
pushNum = true
payload = append(payload, []byte{0x11})
case txscript.OP_12:
pushNum = true
payload = append(payload, []byte{0x12})
case txscript.OP_13:
pushNum = true
payload = append(payload, []byte{0x13})
case txscript.OP_14:
pushNum = true
payload = append(payload, []byte{0x14})
case txscript.OP_15:
pushNum = true
payload = append(payload, []byte{0x15})
case txscript.OP_16:
pushNum = true
payload = append(payload, []byte{0x16})
case txscript.OP_0:
// OP_0 is a special case, it is accepted in ord's implementation
payload = append(payload, []byte{})
default:
data := tokenizer.Data()
if data == nil {
return nil, false
}
payload = append(payload, data)
}
}
// incomplete envelope
if tokenizer.Done() && tokenizer.Opcode() != txscript.OP_ENDIF {
return nil, false
}
// find body (empty data push in even index payload)
bodyIndex := -1
for i, value := range payload {
if i%2 == 0 && len(value) == 0 {
bodyIndex = i
break
}
}
var fieldPayloads [][]byte
var body []byte
if bodyIndex != -1 {
fieldPayloads = payload[:bodyIndex]
body = lo.Flatten(payload[bodyIndex+1:])
} else {
fieldPayloads = payload[:]
}
var incompleteField bool
fields := make(Fields)
for _, chunk := range lo.Chunk(fieldPayloads, 2) {
if len(chunk) != 2 {
incompleteField = true
break
}
key := chunk[0]
value := chunk[1]
// key cannot be empty, as checked by bodyIndex above
tag := Tag(key[0])
fields[tag] = append(fields[tag], value)
}
var duplicateField bool
for _, values := range fields {
if len(values) > 1 {
duplicateField = true
break
}
}
rawContentEncoding := fields.Take(TagContentEncoding)
rawContentType := fields.Take(TagContentType)
rawDelegate := fields.Take(TagDelegate)
rawMetadata := fields.Take(TagMetadata)
rawMetaprotocol := fields.Take(TagMetaprotocol)
rawParent := fields.Take(TagParent)
rawPointer := fields.Take(TagPointer)
unrecognizedEvenField := lo.SomeBy(lo.Keys(fields), func(key Tag) bool {
return key%2 == 0
})
var delegate, parent *InscriptionId
inscriptionId, err := NewInscriptionIdFromString(string(rawDelegate))
if err == nil {
delegate = &inscriptionId
}
inscriptionId, err = NewInscriptionIdFromString(string(rawParent))
if err == nil {
parent = &inscriptionId
}
var pointer *uint64
// if rawPointer is not nil and fits in uint64
if rawPointer != nil && (len(rawPointer) <= 8 || lo.EveryBy(rawPointer[8:], func(value byte) bool {
return value != 0
})) {
// pad zero bytes to 8 bytes
if len(rawPointer) < 8 {
rawPointer = append(rawPointer, make([]byte, 8-len(rawPointer))...)
}
pointer = lo.ToPtr(binary.LittleEndian.Uint64(rawPointer))
}
inscription := Inscription{
Content: body,
ContentEncoding: string(rawContentEncoding),
ContentType: string(rawContentType),
Delegate: delegate,
Metadata: rawMetadata,
Metaprotocol: string(rawMetaprotocol),
Parent: parent,
Pointer: pointer,
}
return &Envelope{
Inscription: inscription,
InputIndex: uint32(inputIndex),
Offset: offset,
PushNum: pushNum,
Stutter: stuttered,
IncompleteField: incompleteField,
DuplicateField: duplicateField,
UnrecognizedEvenField: unrecognizedEvenField,
}, false
}
type Fields map[Tag][][]byte
func (fields Fields) Take(tag Tag) []byte {
values, ok := fields[tag]
if !ok {
return nil
}
if tag.IsChunked() {
delete(fields, tag)
return lo.Flatten(values)
} else {
first := values[0]
values = values[1:]
if len(values) == 0 {
delete(fields, tag)
} else {
fields[tag] = values
}
return first
}
}
func extractTapScript(witness [][]byte) (txscript.ScriptTokenizer, bool) {
witness = removeAnnexFromWitness(witness)
if len(witness) < 2 {
return txscript.ScriptTokenizer{}, false
}
script := witness[len(witness)-2]
return txscript.MakeScriptTokenizer(0, script), true
}
func removeAnnexFromWitness(witness [][]byte) [][]byte {
if len(witness) >= 2 && len(witness[len(witness)-1]) > 0 && witness[len(witness)-1][0] == txscript.TaprootAnnexTag {
return witness[:len(witness)-1]
}
return witness
}

View File

@@ -0,0 +1,742 @@
package ordinals
import (
"testing"
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/core/types"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestParseEnvelopesFromTx(t *testing.T) {
testTx := func(t *testing.T, tx *types.Transaction, expected []*Envelope) {
t.Helper()
envelopes := ParseEnvelopesFromTx(tx)
assert.Equal(t, expected, envelopes)
}
testParseWitness := func(t *testing.T, tapScript []byte, expected []*Envelope) {
t.Helper()
tx := &types.Transaction{
Version: 2,
LockTime: 0,
TxIn: []*types.TxIn{
{
Witness: wire.TxWitness{
tapScript,
{},
},
},
},
}
testTx(t, tx, expected)
}
testEnvelope := func(t *testing.T, payload [][]byte, expected []*Envelope) {
t.Helper()
builder := NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF)
for _, data := range payload {
builder.AddData(data)
}
builder.AddOp(txscript.OP_ENDIF)
script, err := builder.Script()
assert.NoError(t, err)
testParseWitness(
t,
script,
expected,
)
}
t.Run("empty_witness", func(t *testing.T) {
testTx(t, &types.Transaction{
Version: 2,
LockTime: 0,
TxIn: []*types.TxIn{{
Witness: wire.TxWitness{},
}},
}, []*Envelope{})
})
t.Run("ignore_key_path_spends", func(t *testing.T) {
testTx(t, &types.Transaction{
Version: 2,
LockTime: 0,
TxIn: []*types.TxIn{{
Witness: wire.TxWitness{
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script()),
},
}},
}, []*Envelope{})
})
t.Run("ignore_key_path_spends_with_annex", func(t *testing.T) {
testTx(t, &types.Transaction{
Version: 2,
LockTime: 0,
TxIn: []*types.TxIn{{
Witness: wire.TxWitness{
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script()),
[]byte{txscript.TaprootAnnexTag},
},
}},
}, []*Envelope{})
})
t.Run("parse_from_tapscript", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script()),
[]*Envelope{{}},
)
})
t.Run("ignore_unparsable_scripts", func(t *testing.T) {
script := utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script())
script = append(script, 0x01)
testParseWitness(
t,
script,
[]*Envelope{
{},
},
)
})
t.Run("no_inscription", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
Script()),
[]*Envelope{},
)
})
t.Run("duplicate_field", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagNop.Bytes(),
{},
TagNop.Bytes(),
{},
},
[]*Envelope{
{
DuplicateField: true,
},
},
)
})
t.Run("with_content_type", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagBody.Bytes(),
[]byte("ord"),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("with_content_encoding", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagContentEncoding.Bytes(),
[]byte("br"),
TagBody.Bytes(),
[]byte("ord"),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
ContentEncoding: "br",
},
},
},
)
})
t.Run("with_unknown_tag", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagNop.Bytes(),
[]byte("bar"),
TagBody.Bytes(),
[]byte("ord"),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("no_body", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
},
[]*Envelope{
{
Inscription: Inscription{
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("no_content_type", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagBody.Bytes(),
[]byte("foo"),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("foo"),
},
},
},
)
})
t.Run("valid_body_in_multiple_pushes", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagBody.Bytes(),
[]byte("foo"),
[]byte("bar"),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("foobar"),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("valid_body_in_zero_pushes", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagBody.Bytes(),
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte(""),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("valid_body_in_multiple_empty_pushes", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagBody.Bytes(),
{},
{},
{},
{},
{},
{},
{},
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte(""),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("valid_ignore_trailing", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagContentType.Bytes()).
AddData([]byte("text/plain;charset=utf-8")).
AddData(TagBody.Bytes()).
AddData([]byte("ord")).
AddOp(txscript.OP_ENDIF).
AddOp(txscript.OP_CHECKSIG).
Script()),
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("valid_ignore_preceding", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_CHECKSIG).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagContentType.Bytes()).
AddData([]byte("text/plain;charset=utf-8")).
AddData(TagBody.Bytes()).
AddData([]byte("ord")).
AddOp(txscript.OP_ENDIF).
Script()),
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("multiple_inscriptions_in_a_single_witness", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagContentType.Bytes()).
AddData([]byte("text/plain;charset=utf-8")).
AddData(TagBody.Bytes()).
AddData([]byte("foo")).
AddOp(txscript.OP_ENDIF).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagContentType.Bytes()).
AddData([]byte("text/plain;charset=utf-8")).
AddData(TagBody.Bytes()).
AddData([]byte("bar")).
AddOp(txscript.OP_ENDIF).
Script()),
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("foo"),
ContentType: "text/plain;charset=utf-8",
},
},
{
Inscription: Inscription{
Content: []byte("bar"),
ContentType: "text/plain;charset=utf-8",
},
Offset: 1,
},
},
)
})
t.Run("invalid_utf8_does_not_render_inscription_invalid", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("text/plain;charset=utf-8"),
TagBody.Bytes(),
{0b10000000},
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte{0b10000000},
ContentType: "text/plain;charset=utf-8",
},
},
},
)
})
t.Run("no_endif", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
Script()),
[]*Envelope{},
)
})
t.Run("no_op_false", func(t *testing.T) {
testParseWitness(
t,
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script()),
[]*Envelope{},
)
})
t.Run("empty_envelope", func(t *testing.T) {
testEnvelope(
t,
[][]byte{},
[]*Envelope{},
)
})
t.Run("wrong_protocol_identifier", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
[]byte("foo"),
},
[]*Envelope{},
)
})
t.Run("extract_from_second_input", func(t *testing.T) {
testTx(
t,
&types.Transaction{
Version: 2,
LockTime: 0,
TxIn: []*types.TxIn{{}, {
Witness: wire.TxWitness{
utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagContentType.Bytes()).
AddData([]byte("text/plain;charset=utf-8")).
AddData(TagBody.Bytes()).
AddData([]byte("ord")).
AddOp(txscript.OP_ENDIF).
Script(),
),
{},
},
}},
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte("ord"),
ContentType: "text/plain;charset=utf-8",
},
InputIndex: 1,
},
},
)
})
t.Run("inscribe_png", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagContentType.Bytes(),
[]byte("image/png"),
TagBody.Bytes(),
{0x01, 0x02, 0x03},
},
[]*Envelope{
{
Inscription: Inscription{
Content: []byte{0x01, 0x02, 0x03},
ContentType: "image/png",
},
},
},
)
})
t.Run("unknown_odd_fields", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagNop.Bytes(),
{0x00},
},
[]*Envelope{
{
Inscription: Inscription{},
},
},
)
})
t.Run("unknown_even_fields", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagUnbound.Bytes(),
{0x00},
},
[]*Envelope{
{
Inscription: Inscription{},
UnrecognizedEvenField: true,
},
},
)
})
t.Run("pointer_field_is_recognized", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagPointer.Bytes(),
{0x01},
},
[]*Envelope{
{
Inscription: Inscription{
Pointer: lo.ToPtr(uint64(1)),
},
},
},
)
})
t.Run("duplicate_pointer_field_makes_inscription_unbound", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagPointer.Bytes(),
{0x01},
TagPointer.Bytes(),
{0x00},
},
[]*Envelope{
{
Inscription: Inscription{
Pointer: lo.ToPtr(uint64(1)),
},
DuplicateField: true,
UnrecognizedEvenField: true,
},
},
)
})
t.Run("incomplete_field", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagNop.Bytes(),
},
[]*Envelope{
{
Inscription: Inscription{},
IncompleteField: true,
},
},
)
})
t.Run("metadata_is_parsed_correctly", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagMetadata.Bytes(),
{},
},
[]*Envelope{
{
Inscription: Inscription{
Metadata: []byte{},
},
},
},
)
})
t.Run("metadata_is_parsed_correctly_from_chunks", func(t *testing.T) {
testEnvelope(
t,
[][]byte{
protocolId,
TagMetadata.Bytes(),
{0x00},
TagMetadata.Bytes(),
{0x01},
},
[]*Envelope{
{
Inscription: Inscription{
Metadata: []byte{0x00, 0x01},
},
DuplicateField: true,
},
},
)
})
t.Run("pushnum_opcodes_are_parsed_correctly", func(t *testing.T) {
pushNumOpCodes := map[byte][]byte{
txscript.OP_1NEGATE: {0x81},
txscript.OP_1: {0x01},
txscript.OP_2: {0x02},
txscript.OP_3: {0x03},
txscript.OP_4: {0x04},
txscript.OP_5: {0x05},
txscript.OP_6: {0x06},
txscript.OP_7: {0x07},
txscript.OP_8: {0x08},
txscript.OP_9: {0x09},
txscript.OP_10: {0x10},
txscript.OP_11: {0x11},
txscript.OP_12: {0x12},
txscript.OP_13: {0x13},
txscript.OP_14: {0x14},
txscript.OP_15: {0x15},
txscript.OP_16: {0x16},
}
for opCode, value := range pushNumOpCodes {
script := utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddData(TagBody.Bytes()).
AddOp(opCode).
AddOp(txscript.OP_ENDIF).
Script())
testParseWitness(
t,
script,
[]*Envelope{
{
Inscription: Inscription{
Content: value,
},
PushNum: true,
},
},
)
}
})
t.Run("stuttering", func(t *testing.T) {
script := utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script())
testParseWitness(
t,
script,
[]*Envelope{
{
Inscription: Inscription{},
Stutter: true,
},
},
)
script = utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script())
testParseWitness(
t,
script,
[]*Envelope{
{
Inscription: Inscription{},
Stutter: true,
},
},
)
script = utils.Must(NewPushScriptBuilder().
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_AND).
AddOp(txscript.OP_FALSE).
AddOp(txscript.OP_IF).
AddData(protocolId).
AddOp(txscript.OP_ENDIF).
Script())
testParseWitness(
t,
script,
[]*Envelope{
{
Inscription: Inscription{},
Stutter: false,
},
},
)
})
}

View File

@@ -0,0 +1,13 @@
package ordinals
import "github.com/gaze-network/indexer-network/common"
func GetJubileeHeight(network common.Network) uint64 {
switch network {
case common.NetworkMainnet:
return 824544
case common.NetworkTestnet:
return 2544192
}
panic("unsupported network")
}

View File

@@ -0,0 +1,27 @@
package ordinals
import "time"
type Inscription struct {
Content []byte
ContentEncoding string
ContentType string
Delegate *InscriptionId
Metadata []byte
Metaprotocol string
Parent *InscriptionId // in 0.14, inscription has only one parent
Pointer *uint64
}
// TODO: refactor ordinals.InscriptionEntry to entity.InscriptionEntry
type InscriptionEntry struct {
Id InscriptionId
Number int64
SequenceNumber uint64
Cursed bool
CursedForBRC20 bool
CreatedAt time.Time
CreatedAtHeight uint64
Inscription Inscription
TransferCount uint32
}

View File

@@ -0,0 +1,67 @@
package ordinals
import (
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
)
type InscriptionId struct {
TxHash chainhash.Hash
Index uint32
}
func (i InscriptionId) String() string {
return fmt.Sprintf("%si%d", i.TxHash.String(), i.Index)
}
func NewInscriptionId(txHash chainhash.Hash, index uint32) InscriptionId {
return InscriptionId{
TxHash: txHash,
Index: index,
}
}
var ErrInscriptionIdInvalidSeparator = fmt.Errorf("invalid inscription id: must contain exactly one separator")
func NewInscriptionIdFromString(s string) (InscriptionId, error) {
parts := strings.SplitN(s, "i", 2)
if len(parts) != 2 {
return InscriptionId{}, errors.WithStack(ErrInscriptionIdInvalidSeparator)
}
txHash, err := chainhash.NewHashFromStr(parts[0])
if err != nil {
return InscriptionId{}, errors.Wrap(err, "invalid inscription id: cannot parse txHash")
}
index, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return InscriptionId{}, errors.Wrap(err, "invalid inscription id: cannot parse index")
}
return InscriptionId{
TxHash: *txHash,
Index: uint32(index),
}, nil
}
// MarshalJSON implements json.Marshaler
func (r InscriptionId) MarshalJSON() ([]byte, error) {
return []byte(`"` + r.String() + `"`), nil
}
// UnmarshalJSON implements json.Unmarshaler
func (r *InscriptionId) UnmarshalJSON(data []byte) error {
// data must be quoted
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("must be string")
}
data = data[1 : len(data)-1]
parsed, err := NewInscriptionIdFromString(string(data))
if err != nil {
return errors.WithStack(err)
}
*r = parsed
return nil
}

View File

@@ -0,0 +1,109 @@
package ordinals
import (
"testing"
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/stretchr/testify/assert"
)
func TestNewInscriptionIdFromString(t *testing.T) {
tests := []struct {
name string
input string
expected InscriptionId
shouldError bool
}{
{
name: "valid inscription id 1",
input: "1111111111111111111111111111111111111111111111111111111111111111i0",
expected: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 0,
},
},
{
name: "valid inscription id 2",
input: "1111111111111111111111111111111111111111111111111111111111111111i1",
expected: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 1,
},
},
{
name: "valid inscription id 3",
input: "1111111111111111111111111111111111111111111111111111111111111111i4294967295",
expected: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 4294967295,
},
},
{
name: "error no separator",
input: "abc",
shouldError: true,
},
{
name: "error invalid index",
input: "xyzixyz",
shouldError: true,
},
{
name: "error invalid index",
input: "abcixyz",
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := NewInscriptionIdFromString(tt.input)
if tt.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, actual)
}
})
}
}
func TestInscriptionIdString(t *testing.T) {
tests := []struct {
name string
expected string
input InscriptionId
}{
{
name: "valid inscription id 1",
expected: "1111111111111111111111111111111111111111111111111111111111111111i0",
input: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 0,
},
},
{
name: "valid inscription id 2",
expected: "1111111111111111111111111111111111111111111111111111111111111111i1",
input: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 1,
},
},
{
name: "valid inscription id 3",
expected: "1111111111111111111111111111111111111111111111111111111111111111i4294967295",
input: InscriptionId{
TxHash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 4294967295,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.input.String())
})
}
}

View File

@@ -0,0 +1,68 @@
package ordinals
import (
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
)
type SatPoint struct {
OutPoint wire.OutPoint
Offset uint64
}
func (s SatPoint) String() string {
return fmt.Sprintf("%s:%d", s.OutPoint.String(), s.Offset)
}
var ErrSatPointInvalidSeparator = fmt.Errorf("invalid sat point: must contain exactly two separators")
func NewSatPointFromString(s string) (SatPoint, error) {
parts := strings.SplitN(s, ":", 3)
if len(parts) != 3 {
return SatPoint{}, errors.WithStack(ErrSatPointInvalidSeparator)
}
txHash, err := chainhash.NewHashFromStr(parts[0])
if err != nil {
return SatPoint{}, errors.Wrap(err, "invalid inscription id: cannot parse txHash")
}
index, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return SatPoint{}, errors.Wrap(err, "invalid inscription id: cannot parse index")
}
offset, err := strconv.ParseUint(parts[2], 10, 64)
if err != nil {
return SatPoint{}, errors.Wrap(err, "invalid sat point: cannot parse offset")
}
return SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(index),
},
Offset: offset,
}, nil
}
// MarshalJSON implements json.Marshaler
func (r SatPoint) MarshalJSON() ([]byte, error) {
return []byte(`"` + r.String() + `"`), nil
}
// UnmarshalJSON implements json.Unmarshaler
func (r *SatPoint) UnmarshalJSON(data []byte) error {
// data must be quoted
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("must be string")
}
data = data[1 : len(data)-1]
parsed, err := NewSatPointFromString(string(data))
if err != nil {
return errors.WithStack(err)
}
*r = parsed
return nil
}

View File

@@ -0,0 +1,89 @@
package ordinals
import (
"testing"
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/assert"
)
func TestNewSatPointFromString(t *testing.T) {
tests := []struct {
name string
input string
expected SatPoint
shouldError bool
}{
{
name: "valid sat point",
input: "1111111111111111111111111111111111111111111111111111111111111111:1:2",
expected: SatPoint{
OutPoint: wire.OutPoint{
Hash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 1,
},
Offset: 2,
},
},
{
name: "error no separator",
input: "abc",
shouldError: true,
},
{
name: "error invalid output index",
input: "abc:xyz",
shouldError: true,
},
{
name: "error no offset",
input: "1111111111111111111111111111111111111111111111111111111111111111:1",
shouldError: true,
},
{
name: "error invalid offset",
input: "1111111111111111111111111111111111111111111111111111111111111111:1:foo",
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := NewSatPointFromString(tt.input)
if tt.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, actual)
}
})
}
}
func TestSatPointString(t *testing.T) {
tests := []struct {
name string
input SatPoint
expected string
}{
{
name: "valid sat point",
input: SatPoint{
OutPoint: wire.OutPoint{
Hash: *utils.Must(chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")),
Index: 1,
},
Offset: 2,
},
expected: "1111111111111111111111111111111111111111111111111111111111111111:1:2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.input.String())
})
}
}

View File

@@ -0,0 +1,170 @@
package ordinals
import (
"encoding/binary"
"fmt"
"github.com/btcsuite/btcd/txscript"
)
// PushScriptBuilder is a helper to build scripts that requires data pushes to use OP_PUSHDATA* or OP_DATA_* opcodes only.
// Empty data pushes are still encoded as OP_0.
type PushScriptBuilder struct {
script []byte
err error
}
func NewPushScriptBuilder() *PushScriptBuilder {
return &PushScriptBuilder{}
}
// canonicalDataSize returns the number of bytes the canonical encoding of the
// data will take.
func canonicalDataSize(data []byte) int {
dataLen := len(data)
// When the data consists of a single number that can be represented
// by one of the "small integer" opcodes, that opcode will be instead
// of a data push opcode followed by the number.
if dataLen == 0 {
return 1
}
if dataLen < txscript.OP_PUSHDATA1 {
return 1 + dataLen
} else if dataLen <= 0xff {
return 2 + dataLen
} else if dataLen <= 0xffff {
return 3 + dataLen
}
return 5 + dataLen
}
func pushDataToBytes(data []byte) []byte {
if len(data) == 0 {
return []byte{txscript.OP_0}
}
script := make([]byte, 0)
dataLen := len(data)
if dataLen < txscript.OP_PUSHDATA1 {
script = append(script, byte(txscript.OP_DATA_1-1+dataLen))
} else if dataLen <= 0xff {
script = append(script, txscript.OP_PUSHDATA1, byte(dataLen))
} else if dataLen <= 0xffff {
buf := make([]byte, 2)
binary.LittleEndian.PutUint16(buf, uint16(dataLen))
script = append(script, txscript.OP_PUSHDATA2)
script = append(script, buf...)
} else {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, uint32(dataLen))
script = append(script, txscript.OP_PUSHDATA4)
script = append(script, buf...)
}
// Append the actual data.
script = append(script, data...)
return script
}
// AddData pushes the passed data to the end of the script. It automatically
// chooses canonical opcodes depending on the length of the data. A zero length
// buffer will lead to a push of empty data onto the stack (OP_0) and any push
// of data greater than MaxScriptElementSize will not modify the script since
// that is not allowed by the script engine. Also, the script will not be
// modified if pushing the data would cause the script to exceed the maximum
// allowed script engine size.
func (b *PushScriptBuilder) AddData(data []byte) *PushScriptBuilder {
if b.err != nil {
return b
}
// Pushes that would cause the script to exceed the largest allowed
// script size would result in a non-canonical script.
dataSize := canonicalDataSize(data)
if len(b.script)+dataSize > txscript.MaxScriptSize {
str := fmt.Sprintf("adding %d bytes of data would exceed the "+
"maximum allowed canonical script length of %d",
dataSize, txscript.MaxScriptSize)
b.err = txscript.ErrScriptNotCanonical(str)
return b
}
// Pushes larger than the max script element size would result in a
// script that is not canonical.
dataLen := len(data)
if dataLen > txscript.MaxScriptElementSize {
str := fmt.Sprintf("adding a data element of %d bytes would "+
"exceed the maximum allowed script element size of %d",
dataLen, txscript.MaxScriptElementSize)
b.err = txscript.ErrScriptNotCanonical(str)
return b
}
b.script = append(b.script, pushDataToBytes(data)...)
return b
}
// AddFullData should not typically be used by ordinary users as it does not
// include the checks which prevent data pushes larger than the maximum allowed
// sizes which leads to scripts that can't be executed. This is provided for
// testing purposes such as regression tests where sizes are intentionally made
// larger than allowed.
//
// Use AddData instead.
func (b *PushScriptBuilder) AddFullData(data []byte) *PushScriptBuilder {
if b.err != nil {
return b
}
b.script = append(b.script, pushDataToBytes(data)...)
return b
}
// AddOp pushes the passed opcode to the end of the script. The script will not
// be modified if pushing the opcode would cause the script to exceed the
// maximum allowed script engine size.
func (b *PushScriptBuilder) AddOp(opcode byte) *PushScriptBuilder {
if b.err != nil {
return b
}
// Pushes that would cause the script to exceed the largest allowed
// script size would result in a non-canonical script.
if len(b.script)+1 > txscript.MaxScriptSize {
str := fmt.Sprintf("adding an opcode would exceed the maximum "+
"allowed canonical script length of %d", txscript.MaxScriptSize)
b.err = txscript.ErrScriptNotCanonical(str)
return b
}
b.script = append(b.script, opcode)
return b
}
// AddOps pushes the passed opcodes to the end of the script. The script will
// not be modified if pushing the opcodes would cause the script to exceed the
// maximum allowed script engine size.
func (b *PushScriptBuilder) AddOps(opcodes []byte) *PushScriptBuilder {
if b.err != nil {
return b
}
// Pushes that would cause the script to exceed the largest allowed
// script size would result in a non-canonical script.
if len(b.script)+len(opcodes) > txscript.MaxScriptSize {
str := fmt.Sprintf("adding opcodes would exceed the maximum "+
"allowed canonical script length of %d", txscript.MaxScriptSize)
b.err = txscript.ErrScriptNotCanonical(str)
return b
}
b.script = append(b.script, opcodes...)
return b
}
// Script returns the currently built script. When any errors occurred while
// building the script, the script will be returned up the point of the first
// error along with the error.
func (b *PushScriptBuilder) Script() ([]byte, error) {
return b.script, b.err
}

View File

@@ -0,0 +1,81 @@
package ordinals
// Tags represent data fields in a runestone. Unrecognized odd tags are ignored. Unrecognized even tags produce a cenotaph.
type Tag uint8
var (
TagBody = Tag(0)
TagPointer = Tag(2)
// TagUnbound is unrecognized
TagUnbound = Tag(66)
TagContentType = Tag(1)
TagParent = Tag(3)
TagMetadata = Tag(5)
TagMetaprotocol = Tag(7)
TagContentEncoding = Tag(9)
TagDelegate = Tag(11)
// TagNop is unrecognized
TagNop = Tag(255)
)
var allTags = map[Tag]struct{}{
TagPointer: {},
TagContentType: {},
TagParent: {},
TagMetadata: {},
TagMetaprotocol: {},
TagContentEncoding: {},
TagDelegate: {},
}
func (t Tag) IsValid() bool {
_, ok := allTags[t]
return ok
}
var chunkedTags = map[Tag]struct{}{
TagMetadata: {},
}
func (t Tag) IsChunked() bool {
_, ok := chunkedTags[t]
return ok
}
func (t Tag) Bytes() []byte {
if t == TagBody {
return []byte{} // body tag is empty data push
}
return []byte{byte(t)}
}
func ParseTag(input interface{}) (Tag, error) {
switch input := input.(type) {
case Tag:
return input, nil
case int:
return Tag(input), nil
case int8:
return Tag(input), nil
case int16:
return Tag(input), nil
case int32:
return Tag(input), nil
case int64:
return Tag(input), nil
case uint:
return Tag(input), nil
case uint8:
return Tag(input), nil
case uint16:
return Tag(input), nil
case uint32:
return Tag(input), nil
case uint64:
return Tag(input), nil
default:
panic("invalid tag input type")
}
}

View File

@@ -0,0 +1,525 @@
package postgres
import (
"context"
"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/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
)
var _ datagateway.BRC20DataGateway = (*Repository)(nil)
// warning: GetLatestBlock currently returns a types.BlockHeader with only Height and Hash fields populated.
// This is because it is known that all usage of this function only requires these fields. In the future, we may want to populate all fields for type safety.
func (r *Repository) GetLatestBlock(ctx context.Context) (types.BlockHeader, error) {
block, err := r.queries.GetLatestIndexedBlock(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return types.BlockHeader{}, errors.WithStack(errs.NotFound)
}
return types.BlockHeader{}, errors.Wrap(err, "error during query")
}
hash, err := chainhash.NewHashFromStr(block.Hash)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "failed to parse block hash")
}
return types.BlockHeader{
Height: int64(block.Height),
Hash: *hash,
}, nil
}
// GetIndexedBlockByHeight implements datagateway.BRC20DataGateway.
func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error) {
model, err := r.queries.GetIndexedBlockByHeight(ctx, int32(height))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
return nil, errors.Wrap(err, "error during query")
}
indexedBlock, err := mapIndexedBlockModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse indexed block model")
}
return &indexedBlock, nil
}
func (r *Repository) GetProcessorStats(ctx context.Context) (*entity.ProcessorStats, error) {
model, err := r.queries.GetLatestProcessorStats(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
return nil, errors.WithStack(err)
}
stats := mapProcessorStatsModelToType(model)
return &stats, nil
}
func (r *Repository) GetInscriptionTransfersInOutPoints(ctx context.Context, outPoints []wire.OutPoint) (map[ordinals.SatPoint][]*entity.InscriptionTransfer, error) {
txHashArr := lo.Map(outPoints, func(outPoint wire.OutPoint, _ int) string {
return outPoint.Hash.String()
})
txOutIdxArr := lo.Map(outPoints, func(outPoint wire.OutPoint, _ int) int32 {
return int32(outPoint.Index)
})
models, err := r.queries.GetInscriptionTransfersInOutPoints(ctx, gen.GetInscriptionTransfersInOutPointsParams{
TxHashArr: txHashArr,
TxOutIdxArr: txOutIdxArr,
})
if err != nil {
return nil, errors.WithStack(err)
}
results := make(map[ordinals.SatPoint][]*entity.InscriptionTransfer)
for _, model := range models {
inscriptionTransfer, err := mapInscriptionTransferModelToType(model)
if err != nil {
return nil, errors.WithStack(err)
}
results[inscriptionTransfer.NewSatPoint] = append(results[inscriptionTransfer.NewSatPoint], &inscriptionTransfer)
}
return results, nil
}
func (r *Repository) GetInscriptionEntriesByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*ordinals.InscriptionEntry, error) {
idStrs := lo.Map(ids, func(id ordinals.InscriptionId, _ int) string { return id.String() })
models, err := r.queries.GetInscriptionEntriesByIds(ctx, idStrs)
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry)
for _, model := range models {
inscriptionEntry, err := mapInscriptionEntryModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse inscription entry model")
}
result[inscriptionEntry.Id] = &inscriptionEntry
}
return result, nil
}
func (r *Repository) GetInscriptionNumbersByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]int64, error) {
idStrs := lo.Map(ids, func(id ordinals.InscriptionId, _ int) string { return id.String() })
models, err := r.queries.GetInscriptionNumbersByIds(ctx, idStrs)
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[ordinals.InscriptionId]int64)
for _, model := range models {
inscriptionId, err := ordinals.NewInscriptionIdFromString(model.Id)
if err != nil {
return nil, errors.Wrap(err, "failed to parse inscription id")
}
result[inscriptionId] = model.Number
}
return result, nil
}
func (r *Repository) GetInscriptionParentsByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]ordinals.InscriptionId, error) {
idStrs := lo.Map(ids, func(id ordinals.InscriptionId, _ int) string { return id.String() })
models, err := r.queries.GetInscriptionParentsByIds(ctx, idStrs)
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[ordinals.InscriptionId]ordinals.InscriptionId)
for _, model := range models {
if len(model.Parents) == 0 {
// no parent
continue
}
if len(model.Parents) > 1 {
// sanity check, should not happen since 0.14 ord supports only 1 parent
continue
}
inscriptionId, err := ordinals.NewInscriptionIdFromString(model.Id)
if err != nil {
return nil, errors.Wrap(err, "failed to parse inscription id")
}
parentId, err := ordinals.NewInscriptionIdFromString(model.Parents[0])
if err != nil {
return nil, errors.Wrap(err, "failed to parse parent id")
}
result[inscriptionId] = parentId
}
return result, nil
}
func (r *Repository) GetLatestEventId(ctx context.Context) (int64, error) {
row, err := r.queries.GetLatestEventIds(ctx)
if err != nil {
return 0, errors.WithStack(err)
}
return max(row.EventDeployID.(int64), row.EventMintID.(int64), row.EventInscribeTransferID.(int64), row.EventTransferTransferID.(int64)), nil
}
func (r *Repository) GetBalancesBatchAtHeight(ctx context.Context, blockHeight uint64, queries []datagateway.GetBalancesBatchAtHeightQuery) (map[string]map[string]*entity.Balance, error) {
pkScripts := make([]string, 0)
ticks := make([]string, 0)
for _, query := range queries {
pkScripts = append(pkScripts, query.PkScriptHex)
ticks = append(ticks, query.Tick)
}
models, err := r.queries.GetBalancesBatchAtHeight(ctx, gen.GetBalancesBatchAtHeightParams{
PkscriptArr: pkScripts,
TickArr: ticks,
BlockHeight: int32(blockHeight),
})
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[string]map[string]*entity.Balance)
for _, model := range models {
balance, err := mapBalanceModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model")
}
if _, ok := result[model.Pkscript]; !ok {
result[model.Pkscript] = make(map[string]*entity.Balance)
}
result[model.Pkscript][model.Tick] = &balance
}
return result, nil
}
func (r *Repository) GetEventInscribeTransfersByInscriptionIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*entity.EventInscribeTransfer, error) {
idStrs := lo.Map(ids, func(id ordinals.InscriptionId, _ int) string { return id.String() })
models, err := r.queries.GetEventInscribeTransfersByInscriptionIds(ctx, idStrs)
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[ordinals.InscriptionId]*entity.EventInscribeTransfer)
for _, model := range models {
event, err := mapEventInscribeTransferModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse event inscribe transfer model")
}
result[event.InscriptionId] = &event
}
return result, nil
}
func (r *Repository) GetTickEntriesByTicks(ctx context.Context, ticks []string) (map[string]*entity.TickEntry, error) {
models, err := r.queries.GetTickEntriesByTicks(ctx, ticks)
if err != nil {
return nil, errors.WithStack(err)
}
result := make(map[string]*entity.TickEntry)
for _, model := range models {
tickEntry, err := mapTickEntryModelToType(model)
if err != nil {
return nil, errors.Wrap(err, "failed to parse tick entry model")
}
result[tickEntry.Tick] = &tickEntry
}
return result, nil
}
func (r *Repository) CreateIndexedBlock(ctx context.Context, block *entity.IndexedBlock) error {
params := mapIndexedBlockTypeToParams(*block)
if err := r.queries.CreateIndexedBlock(ctx, params); err != nil {
return errors.WithStack(err)
}
return nil
}
func (r *Repository) CreateProcessorStats(ctx context.Context, stats *entity.ProcessorStats) error {
params := mapProcessorStatsTypeToParams(*stats)
if err := r.queries.CreateProcessorStats(ctx, params); err != nil {
return errors.WithStack(err)
}
return nil
}
func (r *Repository) CreateTickEntries(ctx context.Context, blockHeight uint64, entries []*entity.TickEntry) error {
entryParams := make([]gen.CreateTickEntriesParams, 0)
for _, entry := range entries {
params, _, err := mapTickEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map tick entry to create params")
}
entryParams = append(entryParams, params)
}
results := r.queries.CreateTickEntries(ctx, entryParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateTickEntryStates(ctx context.Context, blockHeight uint64, entryStates []*entity.TickEntry) error {
entryParams := make([]gen.CreateTickEntryStatesParams, 0)
for _, entry := range entryStates {
_, params, err := mapTickEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map tick entry to create params")
}
entryParams = append(entryParams, params)
}
results := r.queries.CreateTickEntryStates(ctx, entryParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateInscriptionEntries(ctx context.Context, blockHeight uint64, entries []*ordinals.InscriptionEntry) error {
inscriptionEntryParams := make([]gen.CreateInscriptionEntriesParams, 0)
for _, entry := range entries {
params, _, err := mapInscriptionEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map inscription entry to create params")
}
inscriptionEntryParams = append(inscriptionEntryParams, params)
}
results := r.queries.CreateInscriptionEntries(ctx, inscriptionEntryParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateInscriptionEntryStates(ctx context.Context, blockHeight uint64, entryStates []*ordinals.InscriptionEntry) error {
inscriptionEntryStatesParams := make([]gen.CreateInscriptionEntryStatesParams, 0)
for _, entry := range entryStates {
_, params, err := mapInscriptionEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map inscription entry to create params")
}
inscriptionEntryStatesParams = append(inscriptionEntryStatesParams, params)
}
results := r.queries.CreateInscriptionEntryStates(ctx, inscriptionEntryStatesParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateInscriptionTransfers(ctx context.Context, transfers []*entity.InscriptionTransfer) error {
params := lo.Map(transfers, func(transfer *entity.InscriptionTransfer, _ int) gen.CreateInscriptionTransfersParams {
return mapInscriptionTransferTypeToParams(*transfer)
})
results := r.queries.CreateInscriptionTransfers(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateEventDeploys(ctx context.Context, events []*entity.EventDeploy) error {
params := make([]gen.CreateEventDeploysParams, 0)
for _, event := range events {
param, err := mapEventDeployTypeToParams(*event)
if err != nil {
return errors.Wrap(err, "cannot map event deploy to create params")
}
params = append(params, param)
}
results := r.queries.CreateEventDeploys(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateEventMints(ctx context.Context, events []*entity.EventMint) error {
params := make([]gen.CreateEventMintsParams, 0)
for _, event := range events {
param, err := mapEventMintTypeToParams(*event)
if err != nil {
return errors.Wrap(err, "cannot map event mint to create params")
}
params = append(params, param)
}
results := r.queries.CreateEventMints(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateEventInscribeTransfers(ctx context.Context, events []*entity.EventInscribeTransfer) error {
params := make([]gen.CreateEventInscribeTransfersParams, 0)
for _, event := range events {
param, err := mapEventInscribeTransferTypeToParams(*event)
if err != nil {
return errors.Wrap(err, "cannot map event transfer to create params")
}
params = append(params, param)
}
results := r.queries.CreateEventInscribeTransfers(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateEventTransferTransfers(ctx context.Context, events []*entity.EventTransferTransfer) error {
params := make([]gen.CreateEventTransferTransfersParams, 0)
for _, event := range events {
param, err := mapEventTransferTransferTypeToParams(*event)
if err != nil {
return errors.Wrap(err, "cannot map event transfer to create params")
}
params = append(params, param)
}
results := r.queries.CreateEventTransferTransfers(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) DeleteIndexedBlocksSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteIndexedBlocksSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteProcessorStatsSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteProcessorStatsSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteTickEntriesSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteTickEntriesSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteTickEntryStatesSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteTickEntryStatesSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteEventDeploysSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteEventDeploysSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteEventMintsSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteEventMintsSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteEventInscribeTransfersSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteEventInscribeTransfersSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteEventTransferTransfersSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteEventTransferTransfersSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteBalancesSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteBalancesSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteInscriptionEntriesSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteInscriptionEntriesSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteInscriptionEntryStatesSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteInscriptionEntryStatesSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}
func (r *Repository) DeleteInscriptionTransfersSinceHeight(ctx context.Context, height uint64) error {
if err := r.queries.DeleteInscriptionTransfersSinceHeight(ctx, int32(height)); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}

View File

@@ -0,0 +1,629 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: batch.go
package gen
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
var (
ErrBatchAlreadyClosed = errors.New("batch already closed")
)
const createEventDeploys = `-- name: CreateEventDeploys :batchexec
INSERT INTO "brc20_event_deploys" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "total_supply", "decimals", "limit_per_mint", "is_self_mint") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`
type CreateEventDeploysBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateEventDeploysParams struct {
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
}
func (q *Queries) CreateEventDeploys(ctx context.Context, arg []CreateEventDeploysParams) *CreateEventDeploysBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.InscriptionNumber,
a.Tick,
a.OriginalTick,
a.TxHash,
a.BlockHeight,
a.TxIndex,
a.Timestamp,
a.Pkscript,
a.Satpoint,
a.TotalSupply,
a.Decimals,
a.LimitPerMint,
a.IsSelfMint,
}
batch.Queue(createEventDeploys, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateEventDeploysBatchResults{br, len(arg), false}
}
func (b *CreateEventDeploysBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateEventDeploysBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createEventInscribeTransfers = `-- name: CreateEventInscribeTransfers :batchexec
INSERT INTO "brc20_event_inscribe_transfers" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "output_index", "sats_amount", "amount") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
`
type CreateEventInscribeTransfersBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateEventInscribeTransfersParams struct {
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
OutputIndex int32
SatsAmount int64
Amount pgtype.Numeric
}
func (q *Queries) CreateEventInscribeTransfers(ctx context.Context, arg []CreateEventInscribeTransfersParams) *CreateEventInscribeTransfersBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.InscriptionNumber,
a.Tick,
a.OriginalTick,
a.TxHash,
a.BlockHeight,
a.TxIndex,
a.Timestamp,
a.Pkscript,
a.Satpoint,
a.OutputIndex,
a.SatsAmount,
a.Amount,
}
batch.Queue(createEventInscribeTransfers, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateEventInscribeTransfersBatchResults{br, len(arg), false}
}
func (b *CreateEventInscribeTransfersBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateEventInscribeTransfersBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createEventMints = `-- name: CreateEventMints :batchexec
INSERT INTO "brc20_event_mints" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "pkscript", "satpoint", "amount", "parent_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`
type CreateEventMintsBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateEventMintsParams struct {
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
Amount pgtype.Numeric
ParentID pgtype.Text
}
func (q *Queries) CreateEventMints(ctx context.Context, arg []CreateEventMintsParams) *CreateEventMintsBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.InscriptionNumber,
a.Tick,
a.OriginalTick,
a.TxHash,
a.BlockHeight,
a.TxIndex,
a.Timestamp,
a.Pkscript,
a.Satpoint,
a.Amount,
a.ParentID,
}
batch.Queue(createEventMints, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateEventMintsBatchResults{br, len(arg), false}
}
func (b *CreateEventMintsBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateEventMintsBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createEventTransferTransfers = `-- name: CreateEventTransferTransfers :batchexec
INSERT INTO "brc20_event_transfer_transfers" ("inscription_id", "inscription_number", "tick", "original_tick", "tx_hash", "block_height", "tx_index", "timestamp", "from_pkscript", "from_satpoint", "from_input_index", "to_pkscript", "to_satpoint", "to_output_index", "spent_as_fee", "amount") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
`
type CreateEventTransferTransfersBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateEventTransferTransfersParams struct {
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
FromPkscript string
FromSatpoint string
FromInputIndex int32
ToPkscript string
ToSatpoint string
ToOutputIndex int32
SpentAsFee bool
Amount pgtype.Numeric
}
func (q *Queries) CreateEventTransferTransfers(ctx context.Context, arg []CreateEventTransferTransfersParams) *CreateEventTransferTransfersBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.InscriptionNumber,
a.Tick,
a.OriginalTick,
a.TxHash,
a.BlockHeight,
a.TxIndex,
a.Timestamp,
a.FromPkscript,
a.FromSatpoint,
a.FromInputIndex,
a.ToPkscript,
a.ToSatpoint,
a.ToOutputIndex,
a.SpentAsFee,
a.Amount,
}
batch.Queue(createEventTransferTransfers, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateEventTransferTransfersBatchResults{br, len(arg), false}
}
func (b *CreateEventTransferTransfersBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateEventTransferTransfersBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createInscriptionEntries = `-- name: CreateInscriptionEntries :batchexec
INSERT INTO "brc20_inscription_entries" ("id", "number", "sequence_number", "delegate", "metadata", "metaprotocol", "parents", "pointer", "content", "content_encoding", "content_type", "cursed", "cursed_for_brc20", "created_at", "created_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`
type CreateInscriptionEntriesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionEntriesParams struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
}
func (q *Queries) CreateInscriptionEntries(ctx context.Context, arg []CreateInscriptionEntriesParams) *CreateInscriptionEntriesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Id,
a.Number,
a.SequenceNumber,
a.Delegate,
a.Metadata,
a.Metaprotocol,
a.Parents,
a.Pointer,
a.Content,
a.ContentEncoding,
a.ContentType,
a.Cursed,
a.CursedForBrc20,
a.CreatedAt,
a.CreatedAtHeight,
}
batch.Queue(createInscriptionEntries, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionEntriesBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionEntriesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionEntriesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createInscriptionEntryStates = `-- name: CreateInscriptionEntryStates :batchexec
INSERT INTO "brc20_inscription_entry_states" ("id", "block_height", "transfer_count") VALUES ($1, $2, $3)
`
type CreateInscriptionEntryStatesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionEntryStatesParams struct {
Id string
BlockHeight int32
TransferCount int32
}
func (q *Queries) CreateInscriptionEntryStates(ctx context.Context, arg []CreateInscriptionEntryStatesParams) *CreateInscriptionEntryStatesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Id,
a.BlockHeight,
a.TransferCount,
}
batch.Queue(createInscriptionEntryStates, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionEntryStatesBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionEntryStatesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionEntryStatesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createInscriptionTransfers = `-- name: CreateInscriptionTransfers :batchexec
INSERT INTO "brc20_inscription_transfers" ("inscription_id", "block_height", "tx_index", "tx_hash", "from_input_index", "old_satpoint_tx_hash", "old_satpoint_out_idx", "old_satpoint_offset", "new_satpoint_tx_hash", "new_satpoint_out_idx", "new_satpoint_offset", "new_pkscript", "new_output_value", "sent_as_fee", "transfer_count") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`
type CreateInscriptionTransfersBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionTransfersParams struct {
InscriptionID string
BlockHeight int32
TxIndex int32
TxHash string
FromInputIndex int32
OldSatpointTxHash pgtype.Text
OldSatpointOutIdx pgtype.Int4
OldSatpointOffset pgtype.Int8
NewSatpointTxHash pgtype.Text
NewSatpointOutIdx pgtype.Int4
NewSatpointOffset pgtype.Int8
NewPkscript string
NewOutputValue int64
SentAsFee bool
TransferCount int32
}
func (q *Queries) CreateInscriptionTransfers(ctx context.Context, arg []CreateInscriptionTransfersParams) *CreateInscriptionTransfersBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.BlockHeight,
a.TxIndex,
a.TxHash,
a.FromInputIndex,
a.OldSatpointTxHash,
a.OldSatpointOutIdx,
a.OldSatpointOffset,
a.NewSatpointTxHash,
a.NewSatpointOutIdx,
a.NewSatpointOffset,
a.NewPkscript,
a.NewOutputValue,
a.SentAsFee,
a.TransferCount,
}
batch.Queue(createInscriptionTransfers, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionTransfersBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionTransfersBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionTransfersBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createTickEntries = `-- name: CreateTickEntries :batchexec
INSERT INTO "brc20_tick_entries" ("tick", "original_tick", "total_supply", "decimals", "limit_per_mint", "is_self_mint", "deploy_inscription_id", "deployed_at", "deployed_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type CreateTickEntriesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateTickEntriesParams struct {
Tick string
OriginalTick string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
DeployInscriptionID string
DeployedAt pgtype.Timestamp
DeployedAtHeight int32
}
func (q *Queries) CreateTickEntries(ctx context.Context, arg []CreateTickEntriesParams) *CreateTickEntriesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Tick,
a.OriginalTick,
a.TotalSupply,
a.Decimals,
a.LimitPerMint,
a.IsSelfMint,
a.DeployInscriptionID,
a.DeployedAt,
a.DeployedAtHeight,
}
batch.Queue(createTickEntries, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateTickEntriesBatchResults{br, len(arg), false}
}
func (b *CreateTickEntriesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateTickEntriesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createTickEntryStates = `-- name: CreateTickEntryStates :batchexec
INSERT INTO "brc20_tick_entry_states" ("tick", "block_height", "minted_amount", "burned_amount", "completed_at", "completed_at_height") VALUES ($1, $2, $3, $4, $5, $6)
`
type CreateTickEntryStatesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateTickEntryStatesParams struct {
Tick string
BlockHeight int32
MintedAmount pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) CreateTickEntryStates(ctx context.Context, arg []CreateTickEntryStatesParams) *CreateTickEntryStatesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Tick,
a.BlockHeight,
a.MintedAmount,
a.BurnedAmount,
a.CompletedAt,
a.CompletedAtHeight,
}
batch.Queue(createTickEntryStates, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateTickEntryStatesBatchResults{br, len(arg), false}
}
func (b *CreateTickEntryStatesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateTickEntryStatesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}

View File

@@ -0,0 +1,593 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: data.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createIndexedBlock = `-- name: CreateIndexedBlock :exec
INSERT INTO "brc20_indexed_blocks" ("height", "hash", "event_hash", "cumulative_event_hash") VALUES ($1, $2, $3, $4)
`
type CreateIndexedBlockParams struct {
Height int32
Hash string
EventHash string
CumulativeEventHash string
}
func (q *Queries) CreateIndexedBlock(ctx context.Context, arg CreateIndexedBlockParams) error {
_, err := q.db.Exec(ctx, createIndexedBlock,
arg.Height,
arg.Hash,
arg.EventHash,
arg.CumulativeEventHash,
)
return err
}
const createProcessorStats = `-- name: CreateProcessorStats :exec
INSERT INTO "brc20_processor_stats" ("block_height", "cursed_inscription_count", "blessed_inscription_count", "lost_sats") VALUES ($1, $2, $3, $4)
`
type CreateProcessorStatsParams struct {
BlockHeight int32
CursedInscriptionCount int32
BlessedInscriptionCount int32
LostSats int64
}
func (q *Queries) CreateProcessorStats(ctx context.Context, arg CreateProcessorStatsParams) error {
_, err := q.db.Exec(ctx, createProcessorStats,
arg.BlockHeight,
arg.CursedInscriptionCount,
arg.BlessedInscriptionCount,
arg.LostSats,
)
return err
}
const deleteBalancesSinceHeight = `-- name: DeleteBalancesSinceHeight :exec
DELETE FROM "brc20_balances" WHERE "block_height" >= $1
`
func (q *Queries) DeleteBalancesSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteBalancesSinceHeight, blockHeight)
return err
}
const deleteEventDeploysSinceHeight = `-- name: DeleteEventDeploysSinceHeight :exec
DELETE FROM "brc20_event_deploys" WHERE "block_height" >= $1
`
func (q *Queries) DeleteEventDeploysSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteEventDeploysSinceHeight, blockHeight)
return err
}
const deleteEventInscribeTransfersSinceHeight = `-- name: DeleteEventInscribeTransfersSinceHeight :exec
DELETE FROM "brc20_event_inscribe_transfers" WHERE "block_height" >= $1
`
func (q *Queries) DeleteEventInscribeTransfersSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteEventInscribeTransfersSinceHeight, blockHeight)
return err
}
const deleteEventMintsSinceHeight = `-- name: DeleteEventMintsSinceHeight :exec
DELETE FROM "brc20_event_mints" WHERE "block_height" >= $1
`
func (q *Queries) DeleteEventMintsSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteEventMintsSinceHeight, blockHeight)
return err
}
const deleteEventTransferTransfersSinceHeight = `-- name: DeleteEventTransferTransfersSinceHeight :exec
DELETE FROM "brc20_event_transfer_transfers" WHERE "block_height" >= $1
`
func (q *Queries) DeleteEventTransferTransfersSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteEventTransferTransfersSinceHeight, blockHeight)
return err
}
const deleteIndexedBlocksSinceHeight = `-- name: DeleteIndexedBlocksSinceHeight :exec
DELETE FROM "brc20_indexed_blocks" WHERE "height" >= $1
`
func (q *Queries) DeleteIndexedBlocksSinceHeight(ctx context.Context, height int32) error {
_, err := q.db.Exec(ctx, deleteIndexedBlocksSinceHeight, height)
return err
}
const deleteInscriptionEntriesSinceHeight = `-- name: DeleteInscriptionEntriesSinceHeight :exec
DELETE FROM "brc20_inscription_entries" WHERE "created_at_height" >= $1
`
func (q *Queries) DeleteInscriptionEntriesSinceHeight(ctx context.Context, createdAtHeight int32) error {
_, err := q.db.Exec(ctx, deleteInscriptionEntriesSinceHeight, createdAtHeight)
return err
}
const deleteInscriptionEntryStatesSinceHeight = `-- name: DeleteInscriptionEntryStatesSinceHeight :exec
DELETE FROM "brc20_inscription_entry_states" WHERE "block_height" >= $1
`
func (q *Queries) DeleteInscriptionEntryStatesSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteInscriptionEntryStatesSinceHeight, blockHeight)
return err
}
const deleteInscriptionTransfersSinceHeight = `-- name: DeleteInscriptionTransfersSinceHeight :exec
DELETE FROM "brc20_inscription_transfers" WHERE "block_height" >= $1
`
func (q *Queries) DeleteInscriptionTransfersSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteInscriptionTransfersSinceHeight, blockHeight)
return err
}
const deleteProcessorStatsSinceHeight = `-- name: DeleteProcessorStatsSinceHeight :exec
DELETE FROM "brc20_processor_stats" WHERE "block_height" >= $1
`
func (q *Queries) DeleteProcessorStatsSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteProcessorStatsSinceHeight, blockHeight)
return err
}
const deleteTickEntriesSinceHeight = `-- name: DeleteTickEntriesSinceHeight :exec
DELETE FROM "brc20_tick_entries" WHERE "deployed_at_height" >= $1
`
func (q *Queries) DeleteTickEntriesSinceHeight(ctx context.Context, deployedAtHeight int32) error {
_, err := q.db.Exec(ctx, deleteTickEntriesSinceHeight, deployedAtHeight)
return err
}
const deleteTickEntryStatesSinceHeight = `-- name: DeleteTickEntryStatesSinceHeight :exec
DELETE FROM "brc20_tick_entry_states" WHERE "block_height" >= $1
`
func (q *Queries) DeleteTickEntryStatesSinceHeight(ctx context.Context, blockHeight int32) error {
_, err := q.db.Exec(ctx, deleteTickEntryStatesSinceHeight, blockHeight)
return err
}
const getBalancesBatchAtHeight = `-- name: GetBalancesBatchAtHeight :many
SELECT DISTINCT ON ("brc20_balances"."pkscript", "brc20_balances"."tick") brc20_balances.pkscript, brc20_balances.block_height, brc20_balances.tick, brc20_balances.overall_balance, brc20_balances.available_balance FROM "brc20_balances"
INNER JOIN (
SELECT
unnest($1::text[]) AS "pkscript",
unnest($2::text[]) AS "tick"
) "queries" ON "brc20_balances"."pkscript" = "queries"."pkscript" AND "brc20_balances"."tick" = "queries"."tick" AND "brc20_balances"."block_height" <= $3
ORDER BY "brc20_balances"."pkscript", "brc20_balances"."tick", "block_height" DESC
`
type GetBalancesBatchAtHeightParams struct {
PkscriptArr []string
TickArr []string
BlockHeight int32
}
func (q *Queries) GetBalancesBatchAtHeight(ctx context.Context, arg GetBalancesBatchAtHeightParams) ([]Brc20Balance, error) {
rows, err := q.db.Query(ctx, getBalancesBatchAtHeight, arg.PkscriptArr, arg.TickArr, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20Balance
for rows.Next() {
var i Brc20Balance
if err := rows.Scan(
&i.Pkscript,
&i.BlockHeight,
&i.Tick,
&i.OverallBalance,
&i.AvailableBalance,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEventInscribeTransfersByInscriptionIds = `-- name: GetEventInscribeTransfersByInscriptionIds :many
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, output_index, sats_amount, amount FROM "brc20_event_inscribe_transfers" WHERE "inscription_id" = ANY($1::text[])
`
func (q *Queries) GetEventInscribeTransfersByInscriptionIds(ctx context.Context, inscriptionIds []string) ([]Brc20EventInscribeTransfer, error) {
rows, err := q.db.Query(ctx, getEventInscribeTransfersByInscriptionIds, inscriptionIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20EventInscribeTransfer
for rows.Next() {
var i Brc20EventInscribeTransfer
if err := rows.Scan(
&i.Id,
&i.InscriptionID,
&i.InscriptionNumber,
&i.Tick,
&i.OriginalTick,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.Timestamp,
&i.Pkscript,
&i.Satpoint,
&i.OutputIndex,
&i.SatsAmount,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getIndexedBlockByHeight = `-- name: GetIndexedBlockByHeight :one
SELECT height, hash, event_hash, cumulative_event_hash FROM "brc20_indexed_blocks" WHERE "height" = $1
`
func (q *Queries) GetIndexedBlockByHeight(ctx context.Context, height int32) (Brc20IndexedBlock, error) {
row := q.db.QueryRow(ctx, getIndexedBlockByHeight, height)
var i Brc20IndexedBlock
err := row.Scan(
&i.Height,
&i.Hash,
&i.EventHash,
&i.CumulativeEventHash,
)
return i, err
}
const getInscriptionEntriesByIds = `-- name: GetInscriptionEntriesByIds :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("id") id, block_height, transfer_count FROM "brc20_inscription_entry_states" WHERE "id" = ANY($1::text[]) ORDER BY "id", "block_height" DESC
)
SELECT brc20_inscription_entries.id, number, sequence_number, delegate, metadata, metaprotocol, parents, pointer, content, content_encoding, content_type, cursed, cursed_for_brc20, created_at, created_at_height, states.id, block_height, transfer_count FROM "brc20_inscription_entries"
LEFT JOIN "states" ON "brc20_inscription_entries"."id" = "states"."id"
WHERE "brc20_inscription_entries"."id" = ANY($1::text[])
`
type GetInscriptionEntriesByIdsRow struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
Id_2 pgtype.Text
BlockHeight pgtype.Int4
TransferCount pgtype.Int4
}
func (q *Queries) GetInscriptionEntriesByIds(ctx context.Context, inscriptionIds []string) ([]GetInscriptionEntriesByIdsRow, error) {
rows, err := q.db.Query(ctx, getInscriptionEntriesByIds, inscriptionIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInscriptionEntriesByIdsRow
for rows.Next() {
var i GetInscriptionEntriesByIdsRow
if err := rows.Scan(
&i.Id,
&i.Number,
&i.SequenceNumber,
&i.Delegate,
&i.Metadata,
&i.Metaprotocol,
&i.Parents,
&i.Pointer,
&i.Content,
&i.ContentEncoding,
&i.ContentType,
&i.Cursed,
&i.CursedForBrc20,
&i.CreatedAt,
&i.CreatedAtHeight,
&i.Id_2,
&i.BlockHeight,
&i.TransferCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInscriptionNumbersByIds = `-- name: GetInscriptionNumbersByIds :many
SELECT id, number FROM "brc20_inscription_entries" WHERE "id" = ANY($1::text[])
`
type GetInscriptionNumbersByIdsRow struct {
Id string
Number int64
}
func (q *Queries) GetInscriptionNumbersByIds(ctx context.Context, inscriptionIds []string) ([]GetInscriptionNumbersByIdsRow, error) {
rows, err := q.db.Query(ctx, getInscriptionNumbersByIds, inscriptionIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInscriptionNumbersByIdsRow
for rows.Next() {
var i GetInscriptionNumbersByIdsRow
if err := rows.Scan(&i.Id, &i.Number); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInscriptionParentsByIds = `-- name: GetInscriptionParentsByIds :many
SELECT id, parents FROM "brc20_inscription_entries" WHERE "id" = ANY($1::text[])
`
type GetInscriptionParentsByIdsRow struct {
Id string
Parents []string
}
func (q *Queries) GetInscriptionParentsByIds(ctx context.Context, inscriptionIds []string) ([]GetInscriptionParentsByIdsRow, error) {
rows, err := q.db.Query(ctx, getInscriptionParentsByIds, inscriptionIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInscriptionParentsByIdsRow
for rows.Next() {
var i GetInscriptionParentsByIdsRow
if err := rows.Scan(&i.Id, &i.Parents); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInscriptionTransfersInOutPoints = `-- name: GetInscriptionTransfersInOutPoints :many
SELECT it.inscription_id, it.block_height, it.tx_index, it.tx_hash, it.from_input_index, it.old_satpoint_tx_hash, it.old_satpoint_out_idx, it.old_satpoint_offset, it.new_satpoint_tx_hash, it.new_satpoint_out_idx, it.new_satpoint_offset, it.new_pkscript, it.new_output_value, it.sent_as_fee, it.transfer_count, "ie"."content" FROM (
SELECT
unnest($1::text[]) AS "tx_hash",
unnest($2::int[]) AS "tx_out_idx"
) "inputs"
INNER JOIN "brc20_inscription_transfers" it ON "inputs"."tx_hash" = "it"."new_satpoint_tx_hash" AND "inputs"."tx_out_idx" = "it"."new_satpoint_out_idx"
LEFT JOIN "brc20_inscription_entries" ie ON "it"."inscription_id" = "ie"."id"
`
type GetInscriptionTransfersInOutPointsParams struct {
TxHashArr []string
TxOutIdxArr []int32
}
type GetInscriptionTransfersInOutPointsRow struct {
InscriptionID string
BlockHeight int32
TxIndex int32
TxHash string
FromInputIndex int32
OldSatpointTxHash pgtype.Text
OldSatpointOutIdx pgtype.Int4
OldSatpointOffset pgtype.Int8
NewSatpointTxHash pgtype.Text
NewSatpointOutIdx pgtype.Int4
NewSatpointOffset pgtype.Int8
NewPkscript string
NewOutputValue int64
SentAsFee bool
TransferCount int32
Content []byte
}
func (q *Queries) GetInscriptionTransfersInOutPoints(ctx context.Context, arg GetInscriptionTransfersInOutPointsParams) ([]GetInscriptionTransfersInOutPointsRow, error) {
rows, err := q.db.Query(ctx, getInscriptionTransfersInOutPoints, arg.TxHashArr, arg.TxOutIdxArr)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInscriptionTransfersInOutPointsRow
for rows.Next() {
var i GetInscriptionTransfersInOutPointsRow
if err := rows.Scan(
&i.InscriptionID,
&i.BlockHeight,
&i.TxIndex,
&i.TxHash,
&i.FromInputIndex,
&i.OldSatpointTxHash,
&i.OldSatpointOutIdx,
&i.OldSatpointOffset,
&i.NewSatpointTxHash,
&i.NewSatpointOutIdx,
&i.NewSatpointOffset,
&i.NewPkscript,
&i.NewOutputValue,
&i.SentAsFee,
&i.TransferCount,
&i.Content,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestEventIds = `-- name: GetLatestEventIds :one
WITH "latest_deploy_id" AS (
SELECT "id" FROM "brc20_event_deploys" ORDER BY "id" DESC LIMIT 1
),
"latest_mint_id" AS (
SELECT "id" FROM "brc20_event_mints" ORDER BY "id" DESC LIMIT 1
),
"latest_inscribe_transfer_id" AS (
SELECT "id" FROM "brc20_event_inscribe_transfers" ORDER BY "id" DESC LIMIT 1
),
"latest_transfer_transfer_id" AS (
SELECT "id" FROM "brc20_event_transfer_transfers" ORDER BY "id" DESC LIMIT 1
)
SELECT
COALESCE((SELECT "id" FROM "latest_deploy_id"), -1) AS "event_deploy_id",
COALESCE((SELECT "id" FROM "latest_mint_id"), -1) AS "event_mint_id",
COALESCE((SELECT "id" FROM "latest_inscribe_transfer_id"), -1) AS "event_inscribe_transfer_id",
COALESCE((SELECT "id" FROM "latest_transfer_transfer_id"), -1) AS "event_transfer_transfer_id"
`
type GetLatestEventIdsRow struct {
EventDeployID interface{}
EventMintID interface{}
EventInscribeTransferID interface{}
EventTransferTransferID interface{}
}
func (q *Queries) GetLatestEventIds(ctx context.Context) (GetLatestEventIdsRow, error) {
row := q.db.QueryRow(ctx, getLatestEventIds)
var i GetLatestEventIdsRow
err := row.Scan(
&i.EventDeployID,
&i.EventMintID,
&i.EventInscribeTransferID,
&i.EventTransferTransferID,
)
return i, err
}
const getLatestIndexedBlock = `-- name: GetLatestIndexedBlock :one
SELECT height, hash, event_hash, cumulative_event_hash FROM "brc20_indexed_blocks" ORDER BY "height" DESC LIMIT 1
`
func (q *Queries) GetLatestIndexedBlock(ctx context.Context) (Brc20IndexedBlock, error) {
row := q.db.QueryRow(ctx, getLatestIndexedBlock)
var i Brc20IndexedBlock
err := row.Scan(
&i.Height,
&i.Hash,
&i.EventHash,
&i.CumulativeEventHash,
)
return i, err
}
const getLatestProcessorStats = `-- name: GetLatestProcessorStats :one
SELECT block_height, cursed_inscription_count, blessed_inscription_count, lost_sats FROM "brc20_processor_stats" ORDER BY "block_height" DESC LIMIT 1
`
func (q *Queries) GetLatestProcessorStats(ctx context.Context) (Brc20ProcessorStat, error) {
row := q.db.QueryRow(ctx, getLatestProcessorStats)
var i Brc20ProcessorStat
err := row.Scan(
&i.BlockHeight,
&i.CursedInscriptionCount,
&i.BlessedInscriptionCount,
&i.LostSats,
)
return i, err
}
const getTickEntriesByTicks = `-- name: GetTickEntriesByTicks :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("tick") tick, block_height, minted_amount, burned_amount, completed_at, completed_at_height FROM "brc20_tick_entry_states" WHERE "tick" = ANY($1::text[]) ORDER BY "tick", "block_height" DESC
)
SELECT brc20_tick_entries.tick, original_tick, total_supply, decimals, limit_per_mint, is_self_mint, deploy_inscription_id, deployed_at, deployed_at_height, states.tick, block_height, minted_amount, burned_amount, completed_at, completed_at_height FROM "brc20_tick_entries"
LEFT JOIN "states" ON "brc20_tick_entries"."tick" = "states"."tick"
WHERE "brc20_tick_entries"."tick" = ANY($1::text[])
`
type GetTickEntriesByTicksRow struct {
Tick string
OriginalTick string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
DeployInscriptionID string
DeployedAt pgtype.Timestamp
DeployedAtHeight int32
Tick_2 pgtype.Text
BlockHeight pgtype.Int4
MintedAmount pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}
func (q *Queries) GetTickEntriesByTicks(ctx context.Context, ticks []string) ([]GetTickEntriesByTicksRow, error) {
rows, err := q.db.Query(ctx, getTickEntriesByTicks, ticks)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTickEntriesByTicksRow
for rows.Next() {
var i GetTickEntriesByTicksRow
if err := rows.Scan(
&i.Tick,
&i.OriginalTick,
&i.TotalSupply,
&i.Decimals,
&i.LimitPerMint,
&i.IsSelfMint,
&i.DeployInscriptionID,
&i.DeployedAt,
&i.DeployedAtHeight,
&i.Tick_2,
&i.BlockHeight,
&i.MintedAmount,
&i.BurnedAmount,
&i.CompletedAt,
&i.CompletedAtHeight,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -15,6 +15,7 @@ type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
}
func New(db DBTX) *Queries {

View File

@@ -0,0 +1,49 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: info.sql
package gen
import (
"context"
)
const createIndexerState = `-- name: CreateIndexerState :exec
INSERT INTO brc20_indexer_states (client_version, network, db_version, event_hash_version) VALUES ($1, $2, $3, $4)
`
type CreateIndexerStateParams struct {
ClientVersion string
Network string
DbVersion int32
EventHashVersion int32
}
func (q *Queries) CreateIndexerState(ctx context.Context, arg CreateIndexerStateParams) error {
_, err := q.db.Exec(ctx, createIndexerState,
arg.ClientVersion,
arg.Network,
arg.DbVersion,
arg.EventHashVersion,
)
return err
}
const getLatestIndexerState = `-- name: GetLatestIndexerState :one
SELECT id, client_version, network, db_version, event_hash_version, created_at FROM brc20_indexer_states ORDER BY created_at DESC LIMIT 1
`
func (q *Queries) GetLatestIndexerState(ctx context.Context) (Brc20IndexerState, error) {
row := q.db.QueryRow(ctx, getLatestIndexerState)
var i Brc20IndexerState
err := row.Scan(
&i.Id,
&i.ClientVersion,
&i.Network,
&i.DbVersion,
&i.EventHashVersion,
&i.CreatedAt,
)
return i, err
}

View File

@@ -0,0 +1,174 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package gen
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Brc20Balance struct {
Pkscript string
BlockHeight int32
Tick string
OverallBalance pgtype.Numeric
AvailableBalance pgtype.Numeric
}
type Brc20EventDeploy struct {
Id int64
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
}
type Brc20EventInscribeTransfer struct {
Id int64
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
OutputIndex int32
SatsAmount int64
Amount pgtype.Numeric
}
type Brc20EventMint struct {
Id int64
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
Pkscript string
Satpoint string
Amount pgtype.Numeric
ParentID pgtype.Text
}
type Brc20EventTransferTransfer struct {
Id int64
InscriptionID string
InscriptionNumber int64
Tick string
OriginalTick string
TxHash string
BlockHeight int32
TxIndex int32
Timestamp pgtype.Timestamp
FromPkscript string
FromSatpoint string
FromInputIndex int32
ToPkscript string
ToSatpoint string
ToOutputIndex int32
SpentAsFee bool
Amount pgtype.Numeric
}
type Brc20IndexedBlock struct {
Height int32
Hash string
EventHash string
CumulativeEventHash string
}
type Brc20IndexerState struct {
Id int64
ClientVersion string
Network string
DbVersion int32
EventHashVersion int32
CreatedAt pgtype.Timestamptz
}
type Brc20InscriptionEntry struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
}
type Brc20InscriptionEntryState struct {
Id string
BlockHeight int32
TransferCount int32
}
type Brc20InscriptionTransfer struct {
InscriptionID string
BlockHeight int32
TxIndex int32
TxHash string
FromInputIndex int32
OldSatpointTxHash pgtype.Text
OldSatpointOutIdx pgtype.Int4
OldSatpointOffset pgtype.Int8
NewSatpointTxHash pgtype.Text
NewSatpointOutIdx pgtype.Int4
NewSatpointOffset pgtype.Int8
NewPkscript string
NewOutputValue int64
SentAsFee bool
TransferCount int32
}
type Brc20ProcessorStat struct {
BlockHeight int32
CursedInscriptionCount int32
BlessedInscriptionCount int32
LostSats int64
}
type Brc20TickEntry struct {
Tick string
OriginalTick string
TotalSupply pgtype.Numeric
Decimals int16
LimitPerMint pgtype.Numeric
IsSelfMint bool
DeployInscriptionID string
DeployedAt pgtype.Timestamp
DeployedAtHeight int32
}
type Brc20TickEntryState struct {
Tick string
BlockHeight int32
MintedAmount pgtype.Numeric
BurnedAmount pgtype.Numeric
CompletedAt pgtype.Timestamp
CompletedAtHeight pgtype.Int4
}

View File

@@ -0,0 +1,33 @@
package postgres
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/jackc/pgx/v5"
)
var _ datagateway.IndexerInfoDataGateway = (*Repository)(nil)
func (r *Repository) GetLatestIndexerState(ctx context.Context) (entity.IndexerState, error) {
model, err := r.queries.GetLatestIndexerState(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return entity.IndexerState{}, errors.WithStack(errs.NotFound)
}
return entity.IndexerState{}, errors.Wrap(err, "error during query")
}
state := mapIndexerStatesModelToType(model)
return state, nil
}
func (r *Repository) CreateIndexerState(ctx context.Context, state entity.IndexerState) error {
params := mapIndexerStatesTypeToParams(state)
if err := r.queries.CreateIndexerState(ctx, params); err != nil {
return errors.Wrap(err, "error during exec")
}
return nil
}

View File

@@ -0,0 +1,604 @@
package postgres
import (
"encoding/hex"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
"github.com/shopspring/decimal"
)
func decimalFromNumeric(src pgtype.Numeric) decimal.NullDecimal {
if !src.Valid || src.NaN || src.InfinityModifier != pgtype.Finite {
return decimal.NullDecimal{}
}
result := decimal.NewFromBigInt(src.Int, src.Exp)
return decimal.NewNullDecimal(result)
}
func numericFromDecimal(src decimal.Decimal) pgtype.Numeric {
result := pgtype.Numeric{
Int: src.Coefficient(),
Exp: src.Exponent(),
NaN: false,
InfinityModifier: pgtype.Finite,
Valid: true,
}
return result
}
func numericFromNullDecimal(src decimal.NullDecimal) pgtype.Numeric {
if !src.Valid {
return pgtype.Numeric{}
}
return numericFromDecimal(src.Decimal)
}
func mapIndexerStatesModelToType(src gen.Brc20IndexerState) entity.IndexerState {
var createdAt time.Time
if src.CreatedAt.Valid {
createdAt = src.CreatedAt.Time
}
return entity.IndexerState{
ClientVersion: src.ClientVersion,
Network: common.Network(src.Network),
DBVersion: int32(src.DbVersion),
EventHashVersion: int32(src.EventHashVersion),
CreatedAt: createdAt,
}
}
func mapIndexerStatesTypeToParams(src entity.IndexerState) gen.CreateIndexerStateParams {
return gen.CreateIndexerStateParams{
ClientVersion: src.ClientVersion,
Network: string(src.Network),
DbVersion: int32(src.DBVersion),
EventHashVersion: int32(src.EventHashVersion),
}
}
func mapIndexedBlockModelToType(src gen.Brc20IndexedBlock) (entity.IndexedBlock, error) {
hash, err := chainhash.NewHashFromStr(src.Hash)
if err != nil {
return entity.IndexedBlock{}, errors.Wrap(err, "invalid block hash")
}
eventHash, err := chainhash.NewHashFromStr(src.EventHash)
if err != nil {
return entity.IndexedBlock{}, errors.Wrap(err, "invalid event hash")
}
cumulativeEventHash, err := chainhash.NewHashFromStr(src.CumulativeEventHash)
if err != nil {
return entity.IndexedBlock{}, errors.Wrap(err, "invalid cumulative event hash")
}
return entity.IndexedBlock{
Height: uint64(src.Height),
Hash: *hash,
EventHash: *eventHash,
CumulativeEventHash: *cumulativeEventHash,
}, nil
}
func mapIndexedBlockTypeToParams(src entity.IndexedBlock) gen.CreateIndexedBlockParams {
return gen.CreateIndexedBlockParams{
Height: int32(src.Height),
Hash: src.Hash.String(),
EventHash: src.EventHash.String(),
CumulativeEventHash: src.CumulativeEventHash.String(),
}
}
func mapProcessorStatsModelToType(src gen.Brc20ProcessorStat) entity.ProcessorStats {
return entity.ProcessorStats{
BlockHeight: uint64(src.BlockHeight),
CursedInscriptionCount: uint64(src.CursedInscriptionCount),
BlessedInscriptionCount: uint64(src.BlessedInscriptionCount),
LostSats: uint64(src.LostSats),
}
}
func mapProcessorStatsTypeToParams(src entity.ProcessorStats) gen.CreateProcessorStatsParams {
return gen.CreateProcessorStatsParams{
BlockHeight: int32(src.BlockHeight),
CursedInscriptionCount: int32(src.CursedInscriptionCount),
BlessedInscriptionCount: int32(src.BlessedInscriptionCount),
LostSats: int64(src.LostSats),
}
}
func mapTickEntryModelToType(src gen.GetTickEntriesByTicksRow) (entity.TickEntry, error) {
deployInscriptionId, err := ordinals.NewInscriptionIdFromString(src.DeployInscriptionID)
if err != nil {
return entity.TickEntry{}, errors.Wrap(err, "invalid deployInscriptionId")
}
var completedAt time.Time
if src.CompletedAt.Valid {
completedAt = src.CompletedAt.Time
}
return entity.TickEntry{
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TotalSupply: decimalFromNumeric(src.TotalSupply).Decimal,
Decimals: uint16(src.Decimals),
LimitPerMint: decimalFromNumeric(src.LimitPerMint).Decimal,
IsSelfMint: src.IsSelfMint,
DeployInscriptionId: deployInscriptionId,
DeployedAt: src.DeployedAt.Time,
DeployedAtHeight: uint64(src.DeployedAtHeight),
MintedAmount: decimalFromNumeric(src.MintedAmount).Decimal,
BurnedAmount: decimalFromNumeric(src.BurnedAmount).Decimal,
CompletedAt: completedAt,
CompletedAtHeight: lo.Ternary(src.CompletedAtHeight.Valid, uint64(src.CompletedAtHeight.Int32), 0),
}, nil
}
func mapTickEntryTypeToParams(src entity.TickEntry, blockHeight uint64) (gen.CreateTickEntriesParams, gen.CreateTickEntryStatesParams, error) {
return gen.CreateTickEntriesParams{
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TotalSupply: numericFromDecimal(src.TotalSupply),
Decimals: int16(src.Decimals),
LimitPerMint: numericFromDecimal(src.LimitPerMint),
IsSelfMint: src.IsSelfMint,
DeployInscriptionID: src.DeployInscriptionId.String(),
DeployedAt: pgtype.Timestamp{Time: src.DeployedAt, Valid: true},
DeployedAtHeight: int32(src.DeployedAtHeight),
}, gen.CreateTickEntryStatesParams{
Tick: src.Tick,
BlockHeight: int32(blockHeight),
CompletedAt: pgtype.Timestamp{Time: src.CompletedAt, Valid: !src.CompletedAt.IsZero()},
CompletedAtHeight: pgtype.Int4{Int32: int32(src.CompletedAtHeight), Valid: src.CompletedAtHeight != 0},
MintedAmount: numericFromDecimal(src.MintedAmount),
BurnedAmount: numericFromDecimal(src.BurnedAmount),
}, nil
}
func mapInscriptionEntryModelToType(src gen.GetInscriptionEntriesByIdsRow) (ordinals.InscriptionEntry, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.Id)
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid inscription id")
}
var delegate, parent *ordinals.InscriptionId
if src.Delegate.Valid {
delegateValue, err := ordinals.NewInscriptionIdFromString(src.Delegate.String)
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid delegate id")
}
delegate = &delegateValue
}
// ord 0.14.0 supports only one parent
if len(src.Parents) > 0 {
parentValue, err := ordinals.NewInscriptionIdFromString(src.Parents[0])
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid parent id")
}
parent = &parentValue
}
inscription := ordinals.Inscription{
Content: src.Content,
ContentEncoding: lo.Ternary(src.ContentEncoding.Valid, src.ContentEncoding.String, ""),
ContentType: lo.Ternary(src.ContentType.Valid, src.ContentType.String, ""),
Delegate: delegate,
Metadata: src.Metadata,
Metaprotocol: lo.Ternary(src.Metaprotocol.Valid, src.Metaprotocol.String, ""),
Parent: parent,
Pointer: lo.Ternary(src.Pointer.Valid, lo.ToPtr(uint64(src.Pointer.Int64)), nil),
}
return ordinals.InscriptionEntry{
Id: inscriptionId,
Number: src.Number,
SequenceNumber: uint64(src.SequenceNumber),
Cursed: src.Cursed,
CursedForBRC20: src.CursedForBrc20,
CreatedAt: lo.Ternary(src.CreatedAt.Valid, src.CreatedAt.Time, time.Time{}),
CreatedAtHeight: uint64(src.CreatedAtHeight),
Inscription: inscription,
TransferCount: lo.Ternary(src.TransferCount.Valid, uint32(src.TransferCount.Int32), 0),
}, nil
}
func mapInscriptionEntryTypeToParams(src ordinals.InscriptionEntry, blockHeight uint64) (gen.CreateInscriptionEntriesParams, gen.CreateInscriptionEntryStatesParams, error) {
var delegate, metaprotocol, contentEncoding, contentType pgtype.Text
if src.Inscription.Delegate != nil {
delegate = pgtype.Text{String: src.Inscription.Delegate.String(), Valid: true}
}
if src.Inscription.Metaprotocol != "" {
metaprotocol = pgtype.Text{String: src.Inscription.Metaprotocol, Valid: true}
}
if src.Inscription.ContentEncoding != "" {
contentEncoding = pgtype.Text{String: src.Inscription.ContentEncoding, Valid: true}
}
if src.Inscription.ContentType != "" {
contentType = pgtype.Text{String: src.Inscription.ContentType, Valid: true}
}
var parents []string
if src.Inscription.Parent != nil {
parents = append(parents, src.Inscription.Parent.String())
}
var pointer pgtype.Int8
if src.Inscription.Pointer != nil {
pointer = pgtype.Int8{Int64: int64(*src.Inscription.Pointer), Valid: true}
}
return gen.CreateInscriptionEntriesParams{
Id: src.Id.String(),
Number: src.Number,
SequenceNumber: int64(src.SequenceNumber),
Delegate: delegate,
Metadata: src.Inscription.Metadata,
Metaprotocol: metaprotocol,
Parents: parents,
Pointer: pointer,
Content: src.Inscription.Content,
ContentEncoding: contentEncoding,
ContentType: contentType,
Cursed: src.Cursed,
CursedForBrc20: src.CursedForBRC20,
CreatedAt: lo.Ternary(!src.CreatedAt.IsZero(), pgtype.Timestamp{Time: src.CreatedAt, Valid: true}, pgtype.Timestamp{}),
CreatedAtHeight: int32(src.CreatedAtHeight),
}, gen.CreateInscriptionEntryStatesParams{
Id: src.Id.String(),
BlockHeight: int32(blockHeight),
TransferCount: int32(src.TransferCount),
}, nil
}
func mapInscriptionTransferModelToType(src gen.GetInscriptionTransfersInOutPointsRow) (entity.InscriptionTransfer, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid inscription id")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid tx hash")
}
var oldSatPoint, newSatPoint ordinals.SatPoint
if src.OldSatpointTxHash.Valid {
if !src.OldSatpointOutIdx.Valid || !src.OldSatpointOffset.Valid {
return entity.InscriptionTransfer{}, errors.New("old satpoint out idx and offset must exist if hash exists")
}
txHash, err := chainhash.NewHashFromStr(src.OldSatpointTxHash.String)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid old satpoint tx hash")
}
oldSatPoint = ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.OldSatpointOutIdx.Int32),
},
Offset: uint64(src.OldSatpointOffset.Int64),
}
}
if src.NewSatpointTxHash.Valid {
if !src.NewSatpointOutIdx.Valid || !src.NewSatpointOffset.Valid {
return entity.InscriptionTransfer{}, errors.New("new satpoint out idx and offset must exist if hash exists")
}
txHash, err := chainhash.NewHashFromStr(src.NewSatpointTxHash.String)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid new satpoint tx hash")
}
newSatPoint = ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.NewSatpointOutIdx.Int32),
},
Offset: uint64(src.NewSatpointOffset.Int64),
}
}
newPkScript, err := hex.DecodeString(src.NewPkscript)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "failed to parse pkscript")
}
return entity.InscriptionTransfer{
InscriptionId: inscriptionId,
BlockHeight: uint64(src.BlockHeight),
TxIndex: uint32(src.TxIndex),
TxHash: *txHash,
FromInputIndex: uint32(src.FromInputIndex),
Content: src.Content,
OldSatPoint: oldSatPoint,
NewSatPoint: newSatPoint,
NewPkScript: newPkScript,
NewOutputValue: uint64(src.NewOutputValue),
SentAsFee: src.SentAsFee,
TransferCount: uint32(src.TransferCount),
}, nil
}
func mapInscriptionTransferTypeToParams(src entity.InscriptionTransfer) gen.CreateInscriptionTransfersParams {
return gen.CreateInscriptionTransfersParams{
InscriptionID: src.InscriptionId.String(),
BlockHeight: int32(src.BlockHeight),
TxIndex: int32(src.TxIndex),
TxHash: src.TxHash.String(),
FromInputIndex: int32(src.FromInputIndex),
OldSatpointTxHash: lo.Ternary(src.OldSatPoint != ordinals.SatPoint{}, pgtype.Text{String: src.OldSatPoint.OutPoint.Hash.String(), Valid: true}, pgtype.Text{}),
OldSatpointOutIdx: lo.Ternary(src.OldSatPoint != ordinals.SatPoint{}, pgtype.Int4{Int32: int32(src.OldSatPoint.OutPoint.Index), Valid: true}, pgtype.Int4{}),
OldSatpointOffset: lo.Ternary(src.OldSatPoint != ordinals.SatPoint{}, pgtype.Int8{Int64: int64(src.OldSatPoint.Offset), Valid: true}, pgtype.Int8{}),
NewSatpointTxHash: lo.Ternary(src.NewSatPoint != ordinals.SatPoint{}, pgtype.Text{String: src.NewSatPoint.OutPoint.Hash.String(), Valid: true}, pgtype.Text{}),
NewSatpointOutIdx: lo.Ternary(src.NewSatPoint != ordinals.SatPoint{}, pgtype.Int4{Int32: int32(src.NewSatPoint.OutPoint.Index), Valid: true}, pgtype.Int4{}),
NewSatpointOffset: lo.Ternary(src.NewSatPoint != ordinals.SatPoint{}, pgtype.Int8{Int64: int64(src.NewSatPoint.Offset), Valid: true}, pgtype.Int8{}),
NewPkscript: hex.EncodeToString(src.NewPkScript),
NewOutputValue: int64(src.NewOutputValue),
SentAsFee: src.SentAsFee,
TransferCount: int32(src.TransferCount),
}
}
func mapEventDeployModelToType(src gen.Brc20EventDeploy) (entity.EventDeploy, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.EventDeploy{}, errors.Wrap(err, "invalid inscription id")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.EventDeploy{}, errors.Wrap(err, "invalid tx hash")
}
pkScript, err := hex.DecodeString(src.Pkscript)
if err != nil {
return entity.EventDeploy{}, errors.Wrap(err, "failed to parse pkscript")
}
satPoint, err := ordinals.NewSatPointFromString(src.Satpoint)
if err != nil {
return entity.EventDeploy{}, errors.Wrap(err, "cannot parse satpoint")
}
return entity.EventDeploy{
Id: src.Id,
InscriptionId: inscriptionId,
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: *txHash,
BlockHeight: uint64(src.BlockHeight),
TxIndex: uint32(src.TxIndex),
Timestamp: src.Timestamp.Time,
PkScript: pkScript,
SatPoint: satPoint,
TotalSupply: decimalFromNumeric(src.TotalSupply).Decimal,
Decimals: uint16(src.Decimals),
LimitPerMint: decimalFromNumeric(src.LimitPerMint).Decimal,
IsSelfMint: src.IsSelfMint,
}, nil
}
func mapEventDeployTypeToParams(src entity.EventDeploy) (gen.CreateEventDeploysParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp = pgtype.Timestamp{Time: src.Timestamp, Valid: true}
}
return gen.CreateEventDeploysParams{
InscriptionID: src.InscriptionId.String(),
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: src.TxHash.String(),
BlockHeight: int32(src.BlockHeight),
TxIndex: int32(src.TxIndex),
Timestamp: timestamp,
Pkscript: hex.EncodeToString(src.PkScript),
Satpoint: src.SatPoint.String(),
TotalSupply: numericFromDecimal(src.TotalSupply),
Decimals: int16(src.Decimals),
LimitPerMint: numericFromDecimal(src.LimitPerMint),
IsSelfMint: src.IsSelfMint,
}, nil
}
func mapEventMintModelToType(src gen.Brc20EventMint) (entity.EventMint, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.EventMint{}, errors.Wrap(err, "invalid inscription id")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.EventMint{}, errors.Wrap(err, "invalid tx hash")
}
pkScript, err := hex.DecodeString(src.Pkscript)
if err != nil {
return entity.EventMint{}, errors.Wrap(err, "failed to parse pkscript")
}
satPoint, err := ordinals.NewSatPointFromString(src.Satpoint)
if err != nil {
return entity.EventMint{}, errors.Wrap(err, "cannot parse satpoint")
}
var parentId *ordinals.InscriptionId
if src.ParentID.Valid {
parentIdValue, err := ordinals.NewInscriptionIdFromString(src.ParentID.String)
if err != nil {
return entity.EventMint{}, errors.Wrap(err, "invalid parent id")
}
parentId = &parentIdValue
}
return entity.EventMint{
Id: src.Id,
InscriptionId: inscriptionId,
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: *txHash,
BlockHeight: uint64(src.BlockHeight),
TxIndex: uint32(src.TxIndex),
Timestamp: src.Timestamp.Time,
PkScript: pkScript,
SatPoint: satPoint,
Amount: decimalFromNumeric(src.Amount).Decimal,
ParentId: parentId,
}, nil
}
func mapEventMintTypeToParams(src entity.EventMint) (gen.CreateEventMintsParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp = pgtype.Timestamp{Time: src.Timestamp, Valid: true}
}
var parentId pgtype.Text
if src.ParentId != nil {
parentId = pgtype.Text{String: src.ParentId.String(), Valid: true}
}
return gen.CreateEventMintsParams{
InscriptionID: src.InscriptionId.String(),
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: src.TxHash.String(),
BlockHeight: int32(src.BlockHeight),
TxIndex: int32(src.TxIndex),
Timestamp: timestamp,
Pkscript: hex.EncodeToString(src.PkScript),
Satpoint: src.SatPoint.String(),
Amount: numericFromDecimal(src.Amount),
ParentID: parentId,
}, nil
}
func mapEventInscribeTransferModelToType(src gen.Brc20EventInscribeTransfer) (entity.EventInscribeTransfer, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.EventInscribeTransfer{}, errors.Wrap(err, "cannot parse inscription id")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.EventInscribeTransfer{}, errors.Wrap(err, "cannot parse hash")
}
pkScript, err := hex.DecodeString(src.Pkscript)
if err != nil {
return entity.EventInscribeTransfer{}, errors.Wrap(err, "cannot parse pkScript")
}
satPoint, err := ordinals.NewSatPointFromString(src.Satpoint)
if err != nil {
return entity.EventInscribeTransfer{}, errors.Wrap(err, "cannot parse satPoint")
}
return entity.EventInscribeTransfer{
Id: src.Id,
InscriptionId: inscriptionId,
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: *txHash,
BlockHeight: uint64(src.BlockHeight),
TxIndex: uint32(src.TxIndex),
Timestamp: src.Timestamp.Time,
PkScript: pkScript,
SatPoint: satPoint,
OutputIndex: uint32(src.OutputIndex),
SatsAmount: uint64(src.SatsAmount),
Amount: decimalFromNumeric(src.Amount).Decimal,
}, nil
}
func mapEventInscribeTransferTypeToParams(src entity.EventInscribeTransfer) (gen.CreateEventInscribeTransfersParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp = pgtype.Timestamp{Time: src.Timestamp, Valid: true}
}
return gen.CreateEventInscribeTransfersParams{
InscriptionID: src.InscriptionId.String(),
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: src.TxHash.String(),
BlockHeight: int32(src.BlockHeight),
TxIndex: int32(src.TxIndex),
Timestamp: timestamp,
Pkscript: hex.EncodeToString(src.PkScript),
Satpoint: src.SatPoint.String(),
OutputIndex: int32(src.OutputIndex),
SatsAmount: int64(src.SatsAmount),
Amount: numericFromDecimal(src.Amount),
}, nil
}
func mapEventTransferTransferModelToType(src gen.Brc20EventTransferTransfer) (entity.EventTransferTransfer, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse inscription id")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse hash")
}
fromPkScript, err := hex.DecodeString(src.FromPkscript)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse fromPkScript")
}
fromSatPoint, err := ordinals.NewSatPointFromString(src.FromSatpoint)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse fromSatPoint")
}
toPkScript, err := hex.DecodeString(src.ToPkscript)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse toPkScript")
}
toSatPoint, err := ordinals.NewSatPointFromString(src.ToSatpoint)
if err != nil {
return entity.EventTransferTransfer{}, errors.Wrap(err, "cannot parse toSatPoint")
}
return entity.EventTransferTransfer{
Id: src.Id,
InscriptionId: inscriptionId,
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: *txHash,
BlockHeight: uint64(src.BlockHeight),
TxIndex: uint32(src.TxIndex),
Timestamp: src.Timestamp.Time,
FromPkScript: fromPkScript,
FromSatPoint: fromSatPoint,
FromInputIndex: uint32(src.FromInputIndex),
ToPkScript: toPkScript,
ToSatPoint: toSatPoint,
ToOutputIndex: uint32(src.ToOutputIndex),
SpentAsFee: src.SpentAsFee,
Amount: decimalFromNumeric(src.Amount).Decimal,
}, nil
}
func mapEventTransferTransferTypeToParams(src entity.EventTransferTransfer) (gen.CreateEventTransferTransfersParams, error) {
var timestamp pgtype.Timestamp
if !src.Timestamp.IsZero() {
timestamp = pgtype.Timestamp{Time: src.Timestamp, Valid: true}
}
return gen.CreateEventTransferTransfersParams{
InscriptionID: src.InscriptionId.String(),
InscriptionNumber: src.InscriptionNumber,
Tick: src.Tick,
OriginalTick: src.OriginalTick,
TxHash: src.TxHash.String(),
BlockHeight: int32(src.BlockHeight),
TxIndex: int32(src.TxIndex),
Timestamp: timestamp,
FromPkscript: hex.EncodeToString(src.FromPkScript),
FromSatpoint: src.FromSatPoint.String(),
FromInputIndex: int32(src.FromInputIndex),
ToPkscript: hex.EncodeToString(src.ToPkScript),
ToSatpoint: src.ToSatPoint.String(),
ToOutputIndex: int32(src.ToOutputIndex),
SpentAsFee: src.SpentAsFee,
Amount: numericFromDecimal(src.Amount),
}, nil
}
func mapBalanceModelToType(src gen.Brc20Balance) (entity.Balance, error) {
pkScript, err := hex.DecodeString(src.Pkscript)
if err != nil {
return entity.Balance{}, errors.Wrap(err, "failed to parse pkscript")
}
return entity.Balance{
PkScript: pkScript,
Tick: src.Tick,
BlockHeight: uint64(src.BlockHeight),
OverallBalance: decimalFromNumeric(src.OverallBalance).Decimal,
AvailableBalance: decimalFromNumeric(src.AvailableBalance).Decimal,
}, nil
}

View File

@@ -0,0 +1,20 @@
package postgres
import (
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5"
)
type Repository struct {
db postgres.DB
queries *gen.Queries
tx pgx.Tx
}
func NewRepository(db postgres.DB) *Repository {
return &Repository{
db: db,
queries: gen.New(db),
}
}

View File

@@ -0,0 +1,62 @@
package postgres
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/jackc/pgx/v5"
)
var ErrTxAlreadyExists = errors.New("Transaction already exists. Call Commit() or Rollback() first.")
func (r *Repository) begin(ctx context.Context) (*Repository, error) {
if r.tx != nil {
return nil, errors.WithStack(ErrTxAlreadyExists)
}
tx, err := r.db.Begin(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
return &Repository{
db: r.db,
queries: r.queries.WithTx(tx),
tx: tx,
}, nil
}
func (r *Repository) BeginBRC20Tx(ctx context.Context) (datagateway.BRC20DataGatewayWithTx, error) {
repo, err := r.begin(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
return repo, nil
}
func (r *Repository) Commit(ctx context.Context) error {
if r.tx == nil {
return nil
}
err := r.tx.Commit(ctx)
if err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
r.tx = nil
return nil
}
func (r *Repository) Rollback(ctx context.Context) error {
if r.tx == nil {
return nil
}
err := r.tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
return errors.Wrap(err, "failed to rollback transaction")
}
if err == nil {
logger.DebugContext(ctx, "rolled back transaction")
}
r.tx = nil
return nil
}

236
modules/brc20/processor.go Normal file
View File

@@ -0,0 +1,236 @@
package brc20
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/pkg/btcclient"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/lru"
)
// Make sure to implement the Bitcoin Processor interface
var _ indexer.Processor[*types.Block] = (*Processor)(nil)
type Processor struct {
brc20Dg datagateway.BRC20DataGateway
indexerInfoDg datagateway.IndexerInfoDataGateway
btcClient btcclient.Contract
network common.Network
cleanupFuncs []func(context.Context) error
// block states
flotsamsSentAsFee []*entity.Flotsam
blockReward uint64
// processor stats
cursedInscriptionCount uint64
blessedInscriptionCount uint64
lostSats uint64
// cache
outPointValueCache *lru.Cache[wire.OutPoint, uint64]
// flush buffers - inscription states
newInscriptionTransfers []*entity.InscriptionTransfer
newInscriptionEntries map[ordinals.InscriptionId]*ordinals.InscriptionEntry
newInscriptionEntryStates map[ordinals.InscriptionId]*ordinals.InscriptionEntry
// flush buffers - brc20 states
newTickEntries map[string]*entity.TickEntry
newTickEntryStates map[string]*entity.TickEntry
newEventDeploys []*entity.EventDeploy
newEventMints []*entity.EventMint
newEventInscribeTransfers []*entity.EventInscribeTransfer
newEventTransferTransfers []*entity.EventTransferTransfer
newBalances map[string]map[string]*entity.Balance
}
// TODO: move this to config
const outPointValueCacheSize = 100000
func NewProcessor(brc20Dg datagateway.BRC20DataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, btcClient btcclient.Contract, network common.Network, cleanupFuncs []func(context.Context) error) (*Processor, error) {
outPointValueCache, err := lru.New[wire.OutPoint, uint64](outPointValueCacheSize)
if err != nil {
return nil, errors.Wrap(err, "failed to create outPointValueCache")
}
return &Processor{
brc20Dg: brc20Dg,
indexerInfoDg: indexerInfoDg,
btcClient: btcClient,
network: network,
cleanupFuncs: cleanupFuncs,
flotsamsSentAsFee: make([]*entity.Flotsam, 0),
blockReward: 0,
cursedInscriptionCount: 0, // to be initialized by p.VerifyStates()
blessedInscriptionCount: 0, // to be initialized by p.VerifyStates()
lostSats: 0, // to be initialized by p.VerifyStates()
outPointValueCache: outPointValueCache,
newInscriptionTransfers: make([]*entity.InscriptionTransfer, 0),
newInscriptionEntries: make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry),
newInscriptionEntryStates: make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry),
newTickEntries: make(map[string]*entity.TickEntry),
newTickEntryStates: make(map[string]*entity.TickEntry),
newEventDeploys: make([]*entity.EventDeploy, 0),
newEventMints: make([]*entity.EventMint, 0),
newEventInscribeTransfers: make([]*entity.EventInscribeTransfer, 0),
newEventTransferTransfers: make([]*entity.EventTransferTransfer, 0),
newBalances: make(map[string]map[string]*entity.Balance),
}, nil
}
// VerifyStates implements indexer.Processor.
func (p *Processor) VerifyStates(ctx context.Context) error {
indexerState, err := p.indexerInfoDg.GetLatestIndexerState(ctx)
if err != nil && !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "failed to get latest indexer state")
}
// if not found, create indexer state
if errors.Is(err, errs.NotFound) {
if err := p.indexerInfoDg.CreateIndexerState(ctx, entity.IndexerState{
ClientVersion: ClientVersion,
DBVersion: DBVersion,
EventHashVersion: EventHashVersion,
Network: p.network,
}); err != nil {
return errors.Wrap(err, "failed to set indexer state")
}
} else {
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 != EventHashVersion {
return errors.Wrapf(errs.ConflictSetting, "event version mismatch: current version is %d. Please reset rune's db first.", indexerState.EventHashVersion, EventHashVersion)
}
if indexerState.Network != p.network {
return errors.Wrapf(errs.ConflictSetting, "network mismatch: latest indexed network is %d, configured network is %d. If you want to change the network, please reset the database", indexerState.Network, p.network)
}
}
stats, err := p.brc20Dg.GetProcessorStats(ctx)
if err != nil {
if !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "failed to count cursed inscriptions")
}
stats = &entity.ProcessorStats{
BlockHeight: uint64(startingBlockHeader[p.network].Height),
CursedInscriptionCount: 0,
BlessedInscriptionCount: 0,
LostSats: 0,
}
}
p.cursedInscriptionCount = stats.CursedInscriptionCount
p.blessedInscriptionCount = stats.BlessedInscriptionCount
p.lostSats = stats.LostSats
return nil
}
// CurrentBlock implements indexer.Processor.
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
blockHeader, err := p.brc20Dg.GetLatestBlock(ctx)
if err != nil {
if errors.Is(err, errs.NotFound) {
return startingBlockHeader[p.network], nil
}
return types.BlockHeader{}, errors.Wrap(err, "failed to get latest block")
}
return blockHeader, nil
}
// GetIndexedBlock implements indexer.Processor.
func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error) {
block, err := p.brc20Dg.GetIndexedBlockByHeight(ctx, height)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "failed to get indexed block")
}
return types.BlockHeader{
Height: int64(block.Height),
Hash: block.Hash,
}, nil
}
// Name implements indexer.Processor.
func (p *Processor) Name() string {
return "brc20"
}
// RevertData implements indexer.Processor.
func (p *Processor) RevertData(ctx context.Context, from int64) error {
brc20DgTx, err := p.brc20Dg.BeginBRC20Tx(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer func() {
if err := brc20DgTx.Rollback(ctx); err != nil {
logger.WarnContext(ctx, "failed to rollback transaction",
slogx.Error(err),
slogx.String("event", "rollback_brc20_insertion"),
)
}
}()
if err := brc20DgTx.DeleteIndexedBlocksSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete indexed blocks")
}
if err := brc20DgTx.DeleteProcessorStatsSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete processor stats")
}
if err := brc20DgTx.DeleteTickEntriesSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete ticks")
}
if err := brc20DgTx.DeleteTickEntryStatesSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete tick states")
}
if err := brc20DgTx.DeleteEventDeploysSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete deploy events")
}
if err := brc20DgTx.DeleteEventMintsSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete mint events")
}
if err := brc20DgTx.DeleteEventInscribeTransfersSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete inscribe transfer events")
}
if err := brc20DgTx.DeleteEventTransferTransfersSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete transfer transfer events")
}
if err := brc20DgTx.DeleteBalancesSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete balances")
}
if err := brc20DgTx.DeleteInscriptionEntriesSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete inscription entries")
}
if err := brc20DgTx.DeleteInscriptionEntryStatesSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete inscription entry states")
}
if err := brc20DgTx.DeleteInscriptionTransfersSinceHeight(ctx, uint64(from)); err != nil {
return errors.Wrap(err, "failed to delete inscription transfers")
}
if err := brc20DgTx.Commit(ctx); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
func (p *Processor) Shutdown(ctx context.Context) error {
var errs []error
for _, cleanup := range p.cleanupFuncs {
if err := cleanup(ctx); err != nil {
errs = append(errs, err)
}
}
return errors.WithStack(errors.Join(errs...))
}

View File

@@ -0,0 +1,413 @@
package brc20
import (
"bytes"
"context"
"encoding/hex"
"time"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/brc20"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/samber/lo"
"github.com/shopspring/decimal"
)
func (p *Processor) processBRC20States(ctx context.Context, transfers []*entity.InscriptionTransfer, blockHeader types.BlockHeader) error {
payloads := make([]*brc20.Payload, 0)
ticks := make(map[string]struct{})
for _, transfer := range transfers {
if transfer.Content == nil {
// skip empty content
continue
}
payload, err := brc20.ParsePayload(transfer)
if err != nil {
return errors.Wrap(err, "failed to parse payload")
}
payloads = append(payloads, payload)
ticks[payload.Tick] = struct{}{}
}
if len(payloads) == 0 {
// skip if no valid payloads
return nil
}
// TODO: concurrently fetch from db to optimize speed
tickEntries, err := p.brc20Dg.GetTickEntriesByTicks(ctx, lo.Keys(ticks))
if err != nil {
return errors.Wrap(err, "failed to get inscription entries by ids")
}
// preload required data to reduce individual data fetching during process
inscriptionIds := make([]ordinals.InscriptionId, 0)
inscriptionIdsToFetchParent := make([]ordinals.InscriptionId, 0)
inscriptionIdsToFetchEventInscribeTransfer := make([]ordinals.InscriptionId, 0)
balancesToFetch := make([]datagateway.GetBalancesBatchAtHeightQuery, 0) // pkscript -> tick -> struct{}
for _, payload := range payloads {
inscriptionIds = append(inscriptionIds, payload.Transfer.InscriptionId)
if payload.Op == brc20.OperationMint {
// preload parent id to validate mint events with self mint
if entry := tickEntries[payload.Tick]; entry.IsSelfMint {
inscriptionIdsToFetchParent = append(inscriptionIdsToFetchParent, payload.Transfer.InscriptionId)
}
}
if payload.Op == brc20.OperationTransfer {
if payload.Transfer.OldSatPoint == (ordinals.SatPoint{}) {
// preload balance to validate inscribe transfer event
balancesToFetch = append(balancesToFetch, datagateway.GetBalancesBatchAtHeightQuery{
PkScriptHex: hex.EncodeToString(payload.Transfer.NewPkScript),
Tick: payload.Tick,
})
} else {
// preload inscribe-transfer events to validate transfer-transfer event
inscriptionIdsToFetchEventInscribeTransfer = append(inscriptionIdsToFetchEventInscribeTransfer, payload.Transfer.InscriptionId)
}
}
}
inscriptionIdsToNumber, err := p.getInscriptionNumbersByIds(ctx, lo.Uniq(inscriptionIds))
if err != nil {
return errors.Wrap(err, "failed to get inscription numbers by ids")
}
inscriptionIdsToParent, err := p.getInscriptionParentsByIds(ctx, lo.Uniq(inscriptionIdsToFetchParent))
if err != nil {
return errors.Wrap(err, "failed to get inscription parents by ids")
}
latestEventId, err := p.brc20Dg.GetLatestEventId(ctx)
if err != nil {
return errors.Wrap(err, "failed to get latest event id")
}
// pkscript -> tick -> balance
balances, err := p.brc20Dg.GetBalancesBatchAtHeight(ctx, uint64(blockHeader.Height-1), balancesToFetch)
if err != nil {
return errors.Wrap(err, "failed to get balances batch at height")
}
eventInscribeTransfers, err := p.brc20Dg.GetEventInscribeTransfersByInscriptionIds(ctx, lo.Uniq(inscriptionIdsToFetchEventInscribeTransfer))
if err != nil {
return errors.Wrap(err, "failed to get event inscribe transfers by inscription ids")
}
newTickEntries := make(map[string]*entity.TickEntry)
newTickEntryStates := make(map[string]*entity.TickEntry)
newEventDeploys := make([]*entity.EventDeploy, 0)
newEventMints := make([]*entity.EventMint, 0)
newEventInscribeTransfers := make([]*entity.EventInscribeTransfer, 0)
newEventTransferTransfers := make([]*entity.EventTransferTransfer, 0)
newBalances := make(map[string]map[string]*entity.Balance)
for _, payload := range payloads {
tickEntry := tickEntries[payload.Tick]
if payload.Transfer.SentAsFee && payload.Transfer.OldSatPoint == (ordinals.SatPoint{}) {
// skip inscriptions inscribed as fee
continue
}
switch payload.Op {
case brc20.OperationDeploy:
if payload.Transfer.TransferCount > 1 {
// skip used deploy inscriptions
continue
}
if tickEntry != nil {
// skip deploy inscriptions for duplicate ticks
continue
}
tickEntry := &entity.TickEntry{
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TotalSupply: payload.Max,
Decimals: payload.Dec,
LimitPerMint: payload.Lim,
IsSelfMint: payload.SelfMint,
DeployInscriptionId: payload.Transfer.InscriptionId,
DeployedAt: blockHeader.Timestamp,
DeployedAtHeight: payload.Transfer.BlockHeight,
MintedAmount: decimal.Zero,
BurnedAmount: decimal.Zero,
CompletedAt: time.Time{},
CompletedAtHeight: 0,
}
newTickEntries[payload.Tick] = tickEntry
newTickEntryStates[payload.Tick] = tickEntry
// update entries for other operations in same block
tickEntries[payload.Tick] = tickEntry
newEventDeploys = append(newEventDeploys, &entity.EventDeploy{
Id: latestEventId + 1,
InscriptionId: payload.Transfer.InscriptionId,
InscriptionNumber: inscriptionIdsToNumber[payload.Transfer.InscriptionId],
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TxHash: payload.Transfer.TxHash,
BlockHeight: payload.Transfer.BlockHeight,
TxIndex: payload.Transfer.TxIndex,
Timestamp: blockHeader.Timestamp,
PkScript: payload.Transfer.NewPkScript,
SatPoint: payload.Transfer.NewSatPoint,
TotalSupply: payload.Max,
Decimals: payload.Dec,
LimitPerMint: payload.Lim,
IsSelfMint: payload.SelfMint,
})
latestEventId++
case brc20.OperationMint:
if payload.Transfer.TransferCount > 1 {
// skip used mint inscriptions that are already used
continue
}
if tickEntry == nil {
// skip mint inscriptions for non-existent ticks
continue
}
if -payload.Amt.Exponent() > int32(tickEntry.Decimals) {
// skip mint inscriptions with decimals greater than allowed
continue
}
if tickEntry.MintedAmount.GreaterThanOrEqual(tickEntry.TotalSupply) {
// skip mint inscriptions for ticks with completed mints
continue
}
if payload.Amt.GreaterThan(tickEntry.LimitPerMint) {
// skip mint inscriptions with amount greater than limit per mint
continue
}
mintableAmount := tickEntry.TotalSupply.Sub(tickEntry.MintedAmount)
if payload.Amt.GreaterThan(mintableAmount) {
payload.Amt = mintableAmount
}
var parentId *ordinals.InscriptionId
if tickEntry.IsSelfMint {
parentIdValue, ok := inscriptionIdsToParent[payload.Transfer.InscriptionId]
if !ok {
// skip mint inscriptions for self mint ticks without parent inscription
continue
}
if parentIdValue != tickEntry.DeployInscriptionId {
// skip mint inscriptions for self mint ticks with invalid parent inscription
continue
}
parentId = &parentIdValue
}
tickEntry.MintedAmount = tickEntry.MintedAmount.Add(payload.Amt)
if tickEntry.MintedAmount.GreaterThanOrEqual(tickEntry.TotalSupply) {
tickEntry.CompletedAt = blockHeader.Timestamp
tickEntry.CompletedAtHeight = payload.Transfer.BlockHeight
}
newTickEntryStates[payload.Tick] = tickEntry
newEventMints = append(newEventMints, &entity.EventMint{
Id: latestEventId + 1,
InscriptionId: payload.Transfer.InscriptionId,
InscriptionNumber: inscriptionIdsToNumber[payload.Transfer.InscriptionId],
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TxHash: payload.Transfer.TxHash,
BlockHeight: payload.Transfer.BlockHeight,
TxIndex: payload.Transfer.TxIndex,
Timestamp: blockHeader.Timestamp,
PkScript: payload.Transfer.NewPkScript,
SatPoint: payload.Transfer.NewSatPoint,
Amount: payload.Amt,
ParentId: parentId,
})
latestEventId++
case brc20.OperationTransfer:
if payload.Transfer.TransferCount > 2 {
// skip used transfer inscriptions
continue
}
if tickEntry == nil {
// skip transfer inscriptions for non-existent ticks
continue
}
if -payload.Amt.Exponent() > int32(tickEntry.Decimals) {
// skip transfer inscriptions with decimals greater than allowed
continue
}
if payload.Transfer.OldSatPoint == (ordinals.SatPoint{}) {
// inscribe transfer event
pkScriptHex := hex.EncodeToString(payload.Transfer.NewPkScript)
balance, ok := balances[pkScriptHex][payload.Tick]
if !ok {
balance = &entity.Balance{
PkScript: payload.Transfer.NewPkScript,
Tick: payload.Tick,
BlockHeight: uint64(blockHeader.Height - 1),
OverallBalance: decimal.Zero, // defaults balance to zero if not found
AvailableBalance: decimal.Zero,
}
}
if payload.Amt.GreaterThan(balance.AvailableBalance) {
// skip inscribe transfer event if amount exceeds available balance
continue
}
// update balance state
balance.BlockHeight = uint64(blockHeader.Height)
balance.AvailableBalance = balance.AvailableBalance.Sub(payload.Amt)
if _, ok := balances[pkScriptHex]; !ok {
balances[pkScriptHex] = make(map[string]*entity.Balance)
}
balances[pkScriptHex][payload.Tick] = balance
if _, ok := newBalances[pkScriptHex]; !ok {
newBalances[pkScriptHex] = make(map[string]*entity.Balance)
}
newBalances[pkScriptHex][payload.Tick] = &entity.Balance{}
event := &entity.EventInscribeTransfer{
Id: latestEventId + 1,
InscriptionId: payload.Transfer.InscriptionId,
InscriptionNumber: inscriptionIdsToNumber[payload.Transfer.InscriptionId],
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TxHash: payload.Transfer.TxHash,
BlockHeight: payload.Transfer.BlockHeight,
TxIndex: payload.Transfer.TxIndex,
Timestamp: blockHeader.Timestamp,
PkScript: payload.Transfer.NewPkScript,
SatPoint: payload.Transfer.NewSatPoint,
OutputIndex: payload.Transfer.NewSatPoint.OutPoint.Index,
SatsAmount: payload.Transfer.NewOutputValue,
Amount: payload.Amt,
}
latestEventId++
eventInscribeTransfers[payload.Transfer.InscriptionId] = event
newEventInscribeTransfers = append(newEventInscribeTransfers, event)
} else {
// transfer transfer event
inscribeTransfer, ok := eventInscribeTransfers[payload.Transfer.InscriptionId]
if !ok {
// skip transfer transfer event if prior inscribe transfer event does not exist
continue
}
if payload.Transfer.SentAsFee {
// return balance to sender
fromPkScriptHex := hex.EncodeToString(inscribeTransfer.PkScript)
fromBalance, ok := balances[fromPkScriptHex][payload.Tick]
if !ok {
fromBalance = &entity.Balance{
PkScript: inscribeTransfer.PkScript,
Tick: payload.Tick,
BlockHeight: uint64(blockHeader.Height),
OverallBalance: decimal.Zero, // defaults balance to zero if not found
AvailableBalance: decimal.Zero,
}
}
fromBalance.BlockHeight = uint64(blockHeader.Height)
fromBalance.AvailableBalance = fromBalance.AvailableBalance.Add(payload.Amt)
if _, ok := balances[fromPkScriptHex]; !ok {
balances[fromPkScriptHex] = make(map[string]*entity.Balance)
}
balances[fromPkScriptHex][payload.Tick] = fromBalance
if _, ok := newBalances[fromPkScriptHex]; !ok {
newBalances[fromPkScriptHex] = make(map[string]*entity.Balance)
}
newBalances[fromPkScriptHex][payload.Tick] = fromBalance
newEventTransferTransfers = append(newEventTransferTransfers, &entity.EventTransferTransfer{
Id: latestEventId + 1,
InscriptionId: payload.Transfer.InscriptionId,
InscriptionNumber: inscriptionIdsToNumber[payload.Transfer.InscriptionId],
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TxHash: payload.Transfer.TxHash,
BlockHeight: payload.Transfer.BlockHeight,
TxIndex: payload.Transfer.TxIndex,
Timestamp: blockHeader.Timestamp,
FromPkScript: inscribeTransfer.PkScript,
FromSatPoint: inscribeTransfer.SatPoint,
FromInputIndex: payload.Transfer.FromInputIndex,
ToPkScript: payload.Transfer.NewPkScript,
ToSatPoint: payload.Transfer.NewSatPoint,
ToOutputIndex: payload.Transfer.NewSatPoint.OutPoint.Index,
SpentAsFee: true,
Amount: payload.Amt,
})
} else {
// subtract balance from sender
fromPkScriptHex := hex.EncodeToString(inscribeTransfer.PkScript)
fromBalance, ok := balances[fromPkScriptHex][payload.Tick]
if !ok {
// skip transfer transfer event if from balance does not exist
continue
}
fromBalance.BlockHeight = uint64(blockHeader.Height)
fromBalance.OverallBalance = fromBalance.OverallBalance.Sub(payload.Amt)
if _, ok := balances[fromPkScriptHex]; !ok {
balances[fromPkScriptHex] = make(map[string]*entity.Balance)
}
balances[fromPkScriptHex][payload.Tick] = fromBalance
if _, ok := newBalances[fromPkScriptHex]; !ok {
newBalances[fromPkScriptHex] = make(map[string]*entity.Balance)
}
newBalances[fromPkScriptHex][payload.Tick] = fromBalance
// add balance to receiver
if bytes.Equal(payload.Transfer.NewPkScript, []byte{0x6a}) {
// burn if sent to OP_RETURN
tickEntry.BurnedAmount = tickEntry.BurnedAmount.Add(payload.Amt)
tickEntries[payload.Tick] = tickEntry
newTickEntryStates[payload.Tick] = tickEntry
} else {
toPkScriptHex := hex.EncodeToString(payload.Transfer.NewPkScript)
toBalance, ok := balances[toPkScriptHex][payload.Tick]
if !ok {
toBalance = &entity.Balance{
PkScript: payload.Transfer.NewPkScript,
Tick: payload.Tick,
BlockHeight: uint64(blockHeader.Height),
OverallBalance: decimal.Zero, // defaults balance to zero if not found
AvailableBalance: decimal.Zero,
}
}
toBalance.BlockHeight = uint64(blockHeader.Height)
toBalance.OverallBalance = toBalance.OverallBalance.Add(payload.Amt)
toBalance.AvailableBalance = toBalance.AvailableBalance.Add(payload.Amt)
if _, ok := balances[toPkScriptHex]; !ok {
balances[toPkScriptHex] = make(map[string]*entity.Balance)
}
balances[toPkScriptHex][payload.Tick] = toBalance
if _, ok := newBalances[toPkScriptHex]; !ok {
newBalances[toPkScriptHex] = make(map[string]*entity.Balance)
}
newBalances[toPkScriptHex][payload.Tick] = toBalance
}
newEventTransferTransfers = append(newEventTransferTransfers, &entity.EventTransferTransfer{
Id: latestEventId + 1,
InscriptionId: payload.Transfer.InscriptionId,
InscriptionNumber: inscriptionIdsToNumber[payload.Transfer.InscriptionId],
Tick: payload.Tick,
OriginalTick: payload.OriginalTick,
TxHash: payload.Transfer.TxHash,
BlockHeight: payload.Transfer.BlockHeight,
TxIndex: payload.Transfer.TxIndex,
Timestamp: blockHeader.Timestamp,
FromPkScript: inscribeTransfer.PkScript,
FromSatPoint: inscribeTransfer.SatPoint,
FromInputIndex: payload.Transfer.FromInputIndex,
ToPkScript: payload.Transfer.NewPkScript,
ToSatPoint: payload.Transfer.NewSatPoint,
ToOutputIndex: payload.Transfer.NewSatPoint.OutPoint.Index,
SpentAsFee: false,
Amount: payload.Amt,
})
}
}
}
}
p.newTickEntries = newTickEntries
p.newTickEntryStates = newTickEntryStates
p.newEventDeploys = newEventDeploys
p.newEventMints = newEventMints
p.newEventInscribeTransfers = newEventInscribeTransfers
p.newEventTransferTransfers = newEventTransferTransfers
p.newBalances = newBalances
return nil
}

View File

@@ -0,0 +1,570 @@
package brc20
import (
"context"
"encoding/json"
"slices"
"sync"
"github.com/btcsuite/btcd/blockchain"
"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/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
func (p *Processor) processInscriptionTx(ctx context.Context, tx *types.Transaction, blockHeader types.BlockHeader) error {
ctx = logger.WithContext(ctx, slogx.String("tx_hash", tx.TxHash.String()))
envelopes := ordinals.ParseEnvelopesFromTx(tx)
inputOutPoints := lo.Map(tx.TxIn, func(txIn *types.TxIn, _ int) wire.OutPoint {
return wire.OutPoint{
Hash: txIn.PreviousOutTxHash,
Index: txIn.PreviousOutIndex,
}
})
transfersInOutPoints, err := p.getInscriptionTransfersInOutPoints(ctx, inputOutPoints)
if err != nil {
return errors.Wrap(err, "failed to get inscriptions in outpoints")
}
// cache outpoint values for future blocks
for outIndex, txOut := range tx.TxOut {
p.outPointValueCache.Add(wire.OutPoint{
Hash: tx.TxHash,
Index: uint32(outIndex),
}, uint64(txOut.Value))
}
if len(envelopes) == 0 && len(transfersInOutPoints) == 0 {
// no inscription activity, skip
return nil
}
floatingInscriptions := make([]*entity.Flotsam, 0)
totalInputValue := uint64(0)
totalOutputValue := lo.SumBy(tx.TxOut, func(txOut *types.TxOut) uint64 { return uint64(txOut.Value) })
inscribeOffsets := make(map[uint64]*struct {
inscriptionId ordinals.InscriptionId
count int
})
idCounter := uint32(0)
inputValues, err := p.getOutPointValues(ctx, inputOutPoints)
if err != nil {
return errors.Wrap(err, "failed to get outpoint values")
}
for i, input := range tx.TxIn {
inputOutPoint := wire.OutPoint{
Hash: input.PreviousOutTxHash,
Index: input.PreviousOutIndex,
}
inputValue := inputValues[inputOutPoint]
// skip coinbase inputs since there can't be an inscription in coinbase
if input.PreviousOutTxHash.IsEqual(&chainhash.Hash{}) {
totalInputValue += p.getBlockSubsidy(uint64(tx.BlockHeight))
continue
}
transfersInOutPoint := transfersInOutPoints[inputOutPoint]
for satPoint, transfers := range transfersInOutPoint {
offset := totalInputValue + satPoint.Offset
for _, transfer := range transfers {
floatingInscriptions = append(floatingInscriptions, &entity.Flotsam{
Offset: offset,
InscriptionId: transfer.InscriptionId,
Tx: tx,
OriginOld: &entity.OriginOld{
OldSatPoint: satPoint,
Content: transfer.Content,
InputIndex: uint32(i),
},
})
if _, ok := inscribeOffsets[offset]; !ok {
inscribeOffsets[offset] = &struct {
inscriptionId ordinals.InscriptionId
count int
}{transfer.InscriptionId, 0}
}
inscribeOffsets[offset].count++
}
}
// offset on output to inscribe new inscriptions from this input
offset := totalInputValue
totalInputValue += inputValue
envelopesInInput := lo.Filter(envelopes, func(envelope *ordinals.Envelope, _ int) bool {
return envelope.InputIndex == uint32(i)
})
for _, envelope := range envelopesInInput {
inscriptionId := ordinals.InscriptionId{
TxHash: tx.TxHash,
Index: idCounter,
}
var cursed, cursedForBRC20 bool
if envelope.UnrecognizedEvenField || // unrecognized even field
envelope.DuplicateField || // duplicate field
envelope.IncompleteField || // incomplete field
envelope.InputIndex != 0 || // not first input
envelope.Offset != 0 || // not first envelope in input
envelope.Inscription.Pointer != nil || // contains pointer
envelope.PushNum || // contains pushnum opcodes
envelope.Stutter { // contains stuttering curse structure
cursed = true
cursedForBRC20 = true
}
if initial, ok := inscribeOffsets[offset]; !cursed && ok {
if initial.count > 1 {
cursed = true // reinscription
cursedForBRC20 = true
} else {
initialInscriptionEntry, err := p.getInscriptionEntryById(ctx, initial.inscriptionId)
if err != nil {
return errors.Wrapf(err, "failed to get inscription entry id %s", initial.inscriptionId)
}
if !initialInscriptionEntry.Cursed {
cursed = true // reinscription curse if initial inscription is not cursed
}
if !initialInscriptionEntry.CursedForBRC20 {
cursedForBRC20 = true
}
}
}
// inscriptions are no longer cursed after jubilee, but BRC20 still considers them as cursed
if cursed && uint64(tx.BlockHeight) > ordinals.GetJubileeHeight(p.network) {
cursed = false
}
unbound := inputValue == 0 || envelope.UnrecognizedEvenField
if envelope.Inscription.Pointer != nil && *envelope.Inscription.Pointer < totalOutputValue {
offset = *envelope.Inscription.Pointer
}
floatingInscriptions = append(floatingInscriptions, &entity.Flotsam{
Offset: offset,
InscriptionId: inscriptionId,
Tx: tx,
OriginNew: &entity.OriginNew{
Reinscription: inscribeOffsets[offset] != nil,
Cursed: cursed,
CursedForBRC20: cursedForBRC20,
Fee: 0,
Hidden: false, // we don't care about this field for brc20
Parent: envelope.Inscription.Parent,
Pointer: envelope.Inscription.Pointer,
Unbound: unbound,
Inscription: envelope.Inscription,
},
})
if _, ok := inscribeOffsets[offset]; !ok {
inscribeOffsets[offset] = &struct {
inscriptionId ordinals.InscriptionId
count int
}{inscriptionId, 0}
}
inscribeOffsets[offset].count++
idCounter++
}
}
// parents must exist in floatingInscriptions to be valid
potentialParents := make(map[ordinals.InscriptionId]struct{})
for _, flotsam := range floatingInscriptions {
potentialParents[flotsam.InscriptionId] = struct{}{}
}
for _, flotsam := range floatingInscriptions {
if flotsam.OriginNew != nil && flotsam.OriginNew.Parent != nil {
if _, ok := potentialParents[*flotsam.OriginNew.Parent]; !ok {
// parent not found, ignore parent
flotsam.OriginNew.Parent = nil
}
}
}
// calculate fee for each new inscription
for _, flotsam := range floatingInscriptions {
if flotsam.OriginNew != nil {
flotsam.OriginNew.Fee = (totalInputValue - totalOutputValue) / uint64(idCounter)
}
}
// if tx is coinbase, add inscriptions sent as fee to outputs of this tx
ownInscriptionCount := len(floatingInscriptions)
isCoinbase := tx.TxIn[0].PreviousOutTxHash.IsEqual(&chainhash.Hash{})
if isCoinbase {
floatingInscriptions = append(floatingInscriptions, p.flotsamsSentAsFee...)
}
// sort floatingInscriptions by offset
slices.SortFunc(floatingInscriptions, func(i, j *entity.Flotsam) int {
return int(i.Offset) - int(j.Offset)
})
outputValue := uint64(0)
curIncrIdx := 0
// newLocations := make(map[ordinals.SatPoint][]*Flotsam)
type location struct {
satPoint ordinals.SatPoint
flotsam *entity.Flotsam
sentAsFee bool
}
newLocations := make([]*location, 0)
outputToSumValue := make([]uint64, 0, len(tx.TxOut))
for outIndex, txOut := range tx.TxOut {
end := outputValue + uint64(txOut.Value)
// process all inscriptions that are supposed to be inscribed in this output
for curIncrIdx < len(floatingInscriptions) && floatingInscriptions[curIncrIdx].Offset < end {
newSatPoint := ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: tx.TxHash,
Index: uint32(outIndex),
},
Offset: floatingInscriptions[curIncrIdx].Offset - outputValue,
}
// newLocations[newSatPoint] = append(newLocations[newSatPoint], floatingInscriptions[curIncrIdx])
newLocations = append(newLocations, &location{
satPoint: newSatPoint,
flotsam: floatingInscriptions[curIncrIdx],
sentAsFee: isCoinbase && curIncrIdx >= ownInscriptionCount, // if curIncrIdx >= ownInscriptionCount, then current inscription came from p.flotSamsSentAsFee
})
curIncrIdx++
}
outputValue = end
outputToSumValue = append(outputToSumValue, outputValue)
}
for _, loc := range newLocations {
satPoint := loc.satPoint
flotsam := loc.flotsam
sentAsFee := loc.sentAsFee
// TODO: not sure if we still need to handle pointer here, it's already handled above.
if flotsam.OriginNew != nil && flotsam.OriginNew.Pointer != nil {
pointer := *flotsam.OriginNew.Pointer
for outIndex, outputValue := range outputToSumValue {
start := uint64(0)
if outIndex > 0 {
start = outputToSumValue[outIndex-1]
}
end := outputValue
if start <= pointer && pointer < end {
satPoint.Offset = pointer - start
break
}
}
}
if err := p.updateInscriptionLocation(ctx, satPoint, flotsam, sentAsFee, tx, blockHeader); err != nil {
return errors.Wrap(err, "failed to update inscription location")
}
}
// handle leftover flotsams (flotsams with offset over total output value) )
if isCoinbase {
// if there are leftover inscriptions in coinbase, they are lost permanently
for _, flotsam := range floatingInscriptions[curIncrIdx:] {
newSatPoint := ordinals.SatPoint{
OutPoint: wire.OutPoint{},
Offset: p.lostSats + flotsam.Offset - totalOutputValue,
}
if err := p.updateInscriptionLocation(ctx, newSatPoint, flotsam, false, tx, blockHeader); err != nil {
return errors.Wrap(err, "failed to update inscription location")
}
}
p.lostSats += p.blockReward - totalOutputValue
} else {
// if there are leftover inscriptions in non-coinbase tx, they are stored in p.flotsamsSentAsFee for processing in this block's coinbase tx
for _, flotsam := range floatingInscriptions[curIncrIdx:] {
flotsam.Offset = p.blockReward + flotsam.Offset - totalOutputValue
p.flotsamsSentAsFee = append(p.flotsamsSentAsFee, flotsam)
}
// add fees to block reward
p.blockReward = totalInputValue - totalOutputValue
}
return nil
}
func (p *Processor) updateInscriptionLocation(ctx context.Context, newSatPoint ordinals.SatPoint, flotsam *entity.Flotsam, sentAsFee bool, tx *types.Transaction, blockHeader types.BlockHeader) error {
txOut := tx.TxOut[newSatPoint.OutPoint.Index]
if flotsam.OriginOld != nil {
entry, err := p.getInscriptionEntryById(ctx, flotsam.InscriptionId)
if err != nil {
return errors.Wrapf(err, "failed to get inscription entry id %s", flotsam.InscriptionId)
}
entry.TransferCount++
transfer := &entity.InscriptionTransfer{
InscriptionId: flotsam.InscriptionId,
BlockHeight: uint64(flotsam.Tx.BlockHeight), // use flotsam's tx to track tx that initiated the transfer
TxIndex: flotsam.Tx.Index, // use flotsam's tx to track tx that initiated the transfer
TxHash: flotsam.Tx.TxHash,
Content: flotsam.OriginOld.Content,
FromInputIndex: flotsam.OriginOld.InputIndex,
OldSatPoint: flotsam.OriginOld.OldSatPoint,
NewSatPoint: newSatPoint,
NewPkScript: txOut.PkScript,
NewOutputValue: uint64(txOut.Value),
SentAsFee: sentAsFee,
TransferCount: entry.TransferCount,
}
// track transfers even if transfer count exceeds 2 (because we need to check for reinscriptions)
p.newInscriptionTransfers = append(p.newInscriptionTransfers, transfer)
p.newInscriptionEntryStates[entry.Id] = entry
return nil
}
if flotsam.OriginNew != nil {
origin := flotsam.OriginNew
var inscriptionNumber int64
sequenceNumber := p.cursedInscriptionCount + p.blessedInscriptionCount
if origin.Cursed {
inscriptionNumber = -int64(p.cursedInscriptionCount + 1)
p.cursedInscriptionCount++
} else {
inscriptionNumber = int64(p.blessedInscriptionCount)
p.blessedInscriptionCount++
}
// if not valid brc20 inscription, delete content to save space
if !isBRC20Inscription(origin.Inscription) {
origin.Inscription.Content = nil
origin.Inscription.ContentType = ""
origin.Inscription.ContentEncoding = ""
}
transfer := &entity.InscriptionTransfer{
InscriptionId: flotsam.InscriptionId,
BlockHeight: uint64(flotsam.Tx.BlockHeight), // use flotsam's tx to track tx that initiated the transfer
TxIndex: flotsam.Tx.Index, // use flotsam's tx to track tx that initiated the transfer
TxHash: flotsam.Tx.TxHash,
Content: origin.Inscription.Content,
FromInputIndex: 0, // unused
OldSatPoint: ordinals.SatPoint{},
NewSatPoint: newSatPoint,
NewPkScript: txOut.PkScript,
NewOutputValue: uint64(txOut.Value),
SentAsFee: sentAsFee,
TransferCount: 1, // count inscription as first transfer
}
entry := &ordinals.InscriptionEntry{
Id: flotsam.InscriptionId,
Number: inscriptionNumber,
SequenceNumber: sequenceNumber,
Cursed: origin.Cursed,
CursedForBRC20: origin.CursedForBRC20,
CreatedAt: blockHeader.Timestamp,
CreatedAtHeight: uint64(blockHeader.Height),
Inscription: origin.Inscription,
TransferCount: 1, // count inscription as first transfer
}
p.newInscriptionTransfers = append(p.newInscriptionTransfers, transfer)
p.newInscriptionEntries[entry.Id] = entry
p.newInscriptionEntryStates[entry.Id] = entry
return nil
}
panic("unreachable")
}
type brc20Inscription struct {
P string `json:"p"`
}
func isBRC20Inscription(inscription ordinals.Inscription) bool {
if inscription.ContentType != "application/json" && inscription.ContentType != "text/plain" {
return false
}
// attempt to parse content as json
if inscription.Content == nil {
return false
}
var parsed brc20Inscription
if err := json.Unmarshal(inscription.Content, &parsed); err != nil {
return false
}
if parsed.P != "brc-20" {
return false
}
return true
}
func (p *Processor) getOutPointValues(ctx context.Context, outPoints []wire.OutPoint) (map[wire.OutPoint]uint64, error) {
// try to get from cache if exists
cacheValues := p.outPointValueCache.MGet(outPoints)
result := make(map[wire.OutPoint]uint64)
outPointsToFetch := make([]wire.OutPoint, 0)
for i, outPoint := range outPoints {
if cacheValues[i] != 0 {
result[outPoint] = cacheValues[i]
} else {
outPointsToFetch = append(outPointsToFetch, outPoint)
}
}
eg, ectx := errgroup.WithContext(ctx)
txHashes := make(map[chainhash.Hash]struct{})
for _, outPoint := range outPointsToFetch {
txHashes[outPoint.Hash] = struct{}{}
}
txOutsByHash := make(map[chainhash.Hash][]*types.TxOut)
var mutex sync.Mutex
for txHash := range txHashes {
txHash := txHash
eg.Go(func() error {
txOuts, err := p.btcClient.GetTransactionOutputs(ectx, txHash)
if err != nil {
return errors.Wrap(err, "failed to get transaction outputs")
}
// update cache
mutex.Lock()
defer mutex.Unlock()
txOutsByHash[txHash] = txOuts
for i, txOut := range txOuts {
p.outPointValueCache.Add(wire.OutPoint{Hash: txHash, Index: uint32(i)}, uint64(txOut.Value))
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, errors.WithStack(err)
}
for i := range outPoints {
if result[outPoints[i]] == 0 {
result[outPoints[i]] = uint64(txOutsByHash[outPoints[i].Hash][outPoints[i].Index].Value)
}
}
return result, nil
}
func (p *Processor) getInscriptionTransfersInOutPoints(ctx context.Context, outPoints []wire.OutPoint) (map[wire.OutPoint]map[ordinals.SatPoint][]*entity.InscriptionTransfer, error) {
// try to get from flush buffer if exists
result := make(map[wire.OutPoint]map[ordinals.SatPoint][]*entity.InscriptionTransfer)
outPointsToFetch := make([]wire.OutPoint, 0)
for _, outPoint := range outPoints {
var found bool
for _, transfer := range p.newInscriptionTransfers {
if transfer.NewSatPoint.OutPoint == outPoint {
found = true
if _, ok := result[outPoint]; !ok {
result[outPoint] = make(map[ordinals.SatPoint][]*entity.InscriptionTransfer)
}
result[outPoint][transfer.NewSatPoint] = append(result[outPoint][transfer.NewSatPoint], transfer)
break
}
}
if !found {
outPointsToFetch = append(outPointsToFetch, outPoint)
}
}
transfers, err := p.brc20Dg.GetInscriptionTransfersInOutPoints(ctx, outPointsToFetch)
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}
for satPoint, transferList := range transfers {
if _, ok := result[satPoint.OutPoint]; !ok {
result[satPoint.OutPoint] = make(map[ordinals.SatPoint][]*entity.InscriptionTransfer)
}
result[satPoint.OutPoint][satPoint] = append(result[satPoint.OutPoint][satPoint], transferList...)
}
return result, nil
}
func (p *Processor) getInscriptionEntryById(ctx context.Context, id ordinals.InscriptionId) (*ordinals.InscriptionEntry, error) {
inscriptions, err := p.getInscriptionEntriesByIds(ctx, []ordinals.InscriptionId{id})
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}
inscription, ok := inscriptions[id]
if !ok {
return nil, errors.Wrap(errs.NotFound, "inscription not found")
}
return inscription, nil
}
func (p *Processor) getInscriptionEntriesByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]*ordinals.InscriptionEntry, error) {
// try to get from cache if exists
result := make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry)
idsToFetch := make([]ordinals.InscriptionId, 0)
for _, id := range ids {
if inscriptionEntry, ok := p.newInscriptionEntryStates[id]; ok {
result[id] = inscriptionEntry
} else {
idsToFetch = append(idsToFetch, id)
}
}
if len(idsToFetch) > 0 {
inscriptions, err := p.brc20Dg.GetInscriptionEntriesByIds(ctx, idsToFetch)
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}
for id, inscription := range inscriptions {
result[id] = inscription
}
}
return result, nil
}
func (p *Processor) getInscriptionNumbersByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]int64, error) {
// try to get from cache if exists
result := make(map[ordinals.InscriptionId]int64)
idsToFetch := make([]ordinals.InscriptionId, 0)
for _, id := range ids {
if entry, ok := p.newInscriptionEntryStates[id]; ok {
result[id] = int64(entry.Number)
} else {
idsToFetch = append(idsToFetch, id)
}
}
if len(idsToFetch) > 0 {
inscriptions, err := p.brc20Dg.GetInscriptionNumbersByIds(ctx, idsToFetch)
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}
for id, number := range inscriptions {
result[id] = number
}
}
return result, nil
}
func (p *Processor) getInscriptionParentsByIds(ctx context.Context, ids []ordinals.InscriptionId) (map[ordinals.InscriptionId]ordinals.InscriptionId, error) {
// try to get from cache if exists
result := make(map[ordinals.InscriptionId]ordinals.InscriptionId)
idsToFetch := make([]ordinals.InscriptionId, 0)
for _, id := range ids {
if entry, ok := p.newInscriptionEntryStates[id]; ok {
if entry.Inscription.Parent != nil {
result[id] = *entry.Inscription.Parent
}
} else {
idsToFetch = append(idsToFetch, id)
}
}
if len(idsToFetch) > 0 {
inscriptions, err := p.brc20Dg.GetInscriptionParentsByIds(ctx, idsToFetch)
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}
for id, parent := range inscriptions {
result[id] = parent
}
}
return result, nil
}
func (p *Processor) getBlockSubsidy(blockHeight uint64) uint64 {
return uint64(blockchain.CalcBlockSubsidy(int32(blockHeight), p.network.ChainParams()))
}

View File

@@ -0,0 +1,135 @@
package brc20
import (
"context"
"slices"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/samber/lo"
)
// Process implements indexer.Processor.
func (p *Processor) Process(ctx context.Context, blocks []*types.Block) error {
for _, block := range blocks {
ctx = logger.WithContext(ctx, slogx.Uint64("height", uint64(block.Header.Height)))
logger.DebugContext(ctx, "Processing new block")
p.blockReward = p.getBlockSubsidy(uint64(block.Header.Height))
p.flotsamsSentAsFee = make([]*entity.Flotsam, 0)
// put coinbase tx (first tx) at the end of block
transactions := append(block.Transactions[1:], block.Transactions[0])
for _, tx := range transactions {
if err := p.processInscriptionTx(ctx, tx, block.Header); err != nil {
return errors.Wrap(err, "failed to process tx")
}
}
// sort transfers by tx index, output index, output sat offset
// NOTE: ord indexes inscription transfers spent as fee at the end of the block, but brc20 indexes them as soon as they are sent
slices.SortFunc(p.newInscriptionTransfers, func(t1, t2 *entity.InscriptionTransfer) int {
if t1.TxIndex != t2.TxIndex {
return int(t1.TxIndex) - int(t2.TxIndex)
}
if t1.SentAsFee != t2.SentAsFee {
// transfers sent as fee should be ordered after non-fees
if t1.SentAsFee {
return 1
}
return -1
}
if t1.NewSatPoint.OutPoint.Index != t2.NewSatPoint.OutPoint.Index {
return int(t1.NewSatPoint.OutPoint.Index) - int(t2.NewSatPoint.OutPoint.Index)
}
return int(t1.NewSatPoint.Offset) - int(t2.NewSatPoint.Offset)
})
if err := p.processBRC20States(ctx, p.newInscriptionTransfers, block.Header); err != nil {
return errors.Wrap(err, "failed to process brc20 states")
}
if err := p.flushBlock(ctx, block.Header); err != nil {
return errors.Wrap(err, "failed to flush block")
}
logger.DebugContext(ctx, "Inserted new block")
}
return nil
}
func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeader) error {
brc20DgTx, err := p.brc20Dg.BeginBRC20Tx(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer func() {
if err := brc20DgTx.Rollback(ctx); err != nil {
logger.WarnContext(ctx, "failed to rollback transaction",
slogx.Error(err),
slogx.String("event", "rollback_brc20_insertion"),
)
}
}()
blockHeight := uint64(blockHeader.Height)
// CreateIndexedBlock must be performed before other flush methods to correctly calculate event hash
// TODO: calculate event hash
if err := brc20DgTx.CreateIndexedBlock(ctx, &entity.IndexedBlock{
Height: blockHeight,
Hash: blockHeader.Hash,
EventHash: chainhash.Hash{},
CumulativeEventHash: chainhash.Hash{},
}); err != nil {
return errors.Wrap(err, "failed to create indexed block")
}
// flush new inscription entries
{
newInscriptionEntries := lo.Values(p.newInscriptionEntries)
if err := brc20DgTx.CreateInscriptionEntries(ctx, blockHeight, newInscriptionEntries); err != nil {
return errors.Wrap(err, "failed to create inscription entries")
}
p.newInscriptionEntries = make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry)
}
// flush new inscription entry states
{
newInscriptionEntryStates := lo.Values(p.newInscriptionEntryStates)
if err := brc20DgTx.CreateInscriptionEntryStates(ctx, blockHeight, newInscriptionEntryStates); err != nil {
return errors.Wrap(err, "failed to create inscription entry states")
}
p.newInscriptionEntryStates = make(map[ordinals.InscriptionId]*ordinals.InscriptionEntry)
}
// flush new inscription entry states
{
if err := brc20DgTx.CreateInscriptionTransfers(ctx, p.newInscriptionTransfers); err != nil {
return errors.Wrap(err, "failed to create inscription transfers")
}
p.newInscriptionTransfers = make([]*entity.InscriptionTransfer, 0)
}
// flush processor stats
{
stats := &entity.ProcessorStats{
BlockHeight: blockHeight,
CursedInscriptionCount: p.cursedInscriptionCount,
BlessedInscriptionCount: p.blessedInscriptionCount,
LostSats: p.lostSats,
}
if err := brc20DgTx.CreateProcessorStats(ctx, stats); err != nil {
return errors.Wrap(err, "failed to create processor stats")
}
}
if err := brc20DgTx.Commit(ctx); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}

View File

@@ -1,196 +0,0 @@
package httphandler
import (
"fmt"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/gofiber/fiber/v2"
"google.golang.org/protobuf/encoding/protojson"
)
type handler struct {
repository *repository.Repository
}
func New(repo *repository.Repository) *handler {
h := handler{}
h.repository = repo
return &h
}
func (h *handler) infoHandler(ctx *fiber.Ctx) error {
block, err := h.repository.Queries.GetLastProcessedBlock(ctx.UserContext())
if err != nil {
return fmt.Errorf("Cannot get last processed block : %w", err)
}
err = ctx.JSON(infoResponse{
IndexedBlockHeight: block.BlockHeight,
IndexedBlockHash: block.BlockHash,
})
if err != nil {
return fmt.Errorf("Go fiber cannot parse JSON: %w", err)
}
return nil
}
func (h *handler) deployHandler(ctx *fiber.Ctx) error {
deployId := ctx.Params("deployId")
if deployId == "" {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Go fiber cannot send status: %w", err)
}
return nil
}
var blockHeight, txIndex int32
count, err := fmt.Sscanf(deployId, "%d-%d", &blockHeight, &txIndex)
if count != 2 || err != nil {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Go fiber cannot send status: %w", err)
}
return nil
}
deploys, err := h.repository.Queries.GetNodesale(ctx.UserContext(), gen.GetNodesaleParams{
BlockHeight: blockHeight,
TxIndex: txIndex,
})
if err != nil {
return fmt.Errorf("Cannot get nodesale from db: %w", err)
}
if len(deploys) < 1 {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Go fiber cannot send status: %w", err)
}
return nil
}
deploy := deploys[0]
nodeCount, err := h.repository.Queries.GetNodeCountByTierIndex(ctx.UserContext(), gen.GetNodeCountByTierIndexParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
FromTier: 0,
ToTier: int32(len(deploy.Tiers) - 1),
})
if err != nil {
return fmt.Errorf("Cannot get node count from db : %w", err)
}
tiers := make([]protobuf.Tier, len(deploy.Tiers))
tierResponses := make([]tierResponse, len(deploy.Tiers))
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
return fmt.Errorf("Failed to decode tiers json : %w", err)
}
tierResponses[i].Limit = tiers[i].Limit
tierResponses[i].MaxPerAddress = tiers[i].MaxPerAddress
tierResponses[i].PriceSat = tiers[i].PriceSat
tierResponses[i].Sold = nodeCount[i].Count
}
err = ctx.JSON(&deployResponse{
Id: deployId,
Name: deploy.Name,
StartAt: deploy.StartsAt.Time.UTC(),
EndAt: deploy.EndsAt.Time.UTC(),
Tiers: tierResponses,
SellerPublicKey: deploy.SellerPublicKey,
MaxPerAddress: deploy.MaxPerAddress,
DeployTxHash: deploy.DeployTxHash,
})
if err != nil {
return fmt.Errorf("Go fiber cannot parse JSON: %w", err)
}
return nil
}
func (h *handler) nodesHandler(ctx *fiber.Ctx) error {
deployId := ctx.Query("deployId")
if deployId == "" {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Go fiber cannot send status: %w", err)
}
return nil
}
ownerPublicKey := ctx.Query("ownerPublicKey")
delegateePublicKey := ctx.Query("delegateePublicKey")
var blockHeight, txIndex int32
count, err := fmt.Sscanf(deployId, "%d-%d", &blockHeight, &txIndex)
if count != 2 || err != nil {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Go fiber cannot send status: %w", err)
}
return nil
}
nodes, err := h.repository.Queries.GetNodesByPubkey(ctx.UserContext(), gen.GetNodesByPubkeyParams{
SaleBlock: blockHeight,
SaleTxIndex: txIndex,
OwnerPublicKey: ownerPublicKey,
DelegatedTo: delegateePublicKey,
})
if err != nil {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Can't get nodes from db: %w", err)
}
return nil
}
responses := make([]nodeResponse, len(nodes))
for i, node := range nodes {
responses[i].DeployId = deployId
responses[i].NodeId = node.NodeID
responses[i].TierIndex = node.TierIndex
responses[i].DelegatedTo = node.DelegatedTo
responses[i].OwnerPublicKey = node.OwnerPublicKey
responses[i].PurchaseTxHash = node.PurchaseTxHash
responses[i].DelegateTxHash = node.DelegateTxHash
responses[i].PurchaseBlockHeight = node.TxIndex
}
err = ctx.JSON(responses)
if err != nil {
return fmt.Errorf("Go fiber cannot parse JSON: %w", err)
}
return nil
}
func (h *handler) eventsHandler(ctx *fiber.Ctx) error {
walletAddress := ctx.Query("walletAddress")
events, err := h.repository.Queries.GetEventsByWallet(ctx.UserContext(), walletAddress)
if err != nil {
err := ctx.SendStatus(404)
if err != nil {
return fmt.Errorf("Can't get events from db: %w", err)
}
return nil
}
responses := make([]eventResposne, len(events))
for i, event := range events {
responses[i].TxHash = event.TxHash
responses[i].BlockHeight = event.BlockHeight
responses[i].TxIndex = event.TxIndex
responses[i].WalletAddress = event.WalletAddress
responses[i].Action = protobuf.Action_name[event.Action]
responses[i].ParsedMessage = event.ParsedMessage
responses[i].BlockTimestamp = event.BlockTimestamp.Time.UTC()
responses[i].BlockHash = event.BlockHash
}
err = ctx.JSON(responses)
if err != nil {
return fmt.Errorf("Go fiber cannot parse JSON: %w", err)
}
return nil
}

View File

@@ -1,51 +0,0 @@
package httphandler
import (
"encoding/json"
"time"
)
type infoResponse struct {
IndexedBlockHeight int32 `json:"indexedBlockHeight"`
IndexedBlockHash string `json:"indexedBlockHash"`
}
type deployResponse struct {
Id string `json:"id"`
Name string `json:"name"`
StartAt time.Time `json:"startAt"`
EndAt time.Time `json:"EndAt"`
Tiers []tierResponse `json:"tiers"`
SellerPublicKey string `json:"sellerPublicKey"`
MaxPerAddress int32 `json:"maxPerAddress"`
DeployTxHash string `json:"deployTxHash"`
}
type tierResponse struct {
PriceSat uint32 `json:"priceSat"`
Limit uint32 `json:"limit"`
MaxPerAddress uint32 `json:"maxPerAddress"`
Sold int64 `json:"sold"`
}
type nodeResponse struct {
DeployId string `json:"deployId"`
NodeId int32 `json:"nodeId"`
TierIndex int32 `json:"tierIndex"`
DelegatedTo string `json:"delegatedTo"`
OwnerPublicKey string `json:"ownerPublicKey"`
PurchaseTxHash string `json:"purchaseTxHash"`
DelegateTxHash string `json:"delegateTxHash"`
PurchaseBlockHeight int32 `json:"purchaseBlockHeight"`
}
type eventResposne struct {
TxHash string `json:"txHash"`
BlockHeight int32 `json:"blockHeight"`
TxIndex int32 `json:"txIndex"`
WalletAddress string `json:"walletAddress"`
Action string `json:"action"`
ParsedMessage json.RawMessage `json:"parsedMessage"`
BlockTimestamp time.Time `json:"blockTimestamp"`
BlockHash string `json:"blockHash"`
}

View File

@@ -1,16 +0,0 @@
package httphandler
import (
"github.com/gofiber/fiber/v2"
)
func (h *handler) Mount(router fiber.Router) error {
r := router.Group("/nodesale/v1")
r.Get("/info", h.infoHandler)
r.Get("/deploy/:deployId", h.deployHandler)
r.Get("/nodes", h.nodesHandler)
r.Get("/events", h.eventsHandler)
return nil
}

View File

@@ -1,7 +0,0 @@
package config
import "github.com/gaze-network/indexer-network/internal/postgres"
type Config struct {
Postgres postgres.Config `mapstructure:"postgres"`
}

View File

@@ -1,9 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS nodes;
DROP TABLE IF EXISTS node_sales;
DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS blocks;
COMMIT;

View File

@@ -1,63 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS blocks (
"block_height" INTEGER NOT NULL,
"block_hash" TEXT NOT NULL,
"module" TEXT NOT NULL,
PRIMARY KEY("block_height", "block_hash")
);
CREATE TABLE IF NOT EXISTS events (
"tx_hash" TEXT NOT NULL PRIMARY KEY,
"block_height" INTEGER NOT NULL,
"tx_index" INTEGER NOT NULL,
"wallet_address" TEXT NOT NULL,
"valid" BOOLEAN NOT NULL,
"action" INTEGER NOT NULL,
"raw_message" BYTEA NOT NULL,
"parsed_message" JSONB NOT NULL,
"block_timestamp" TIMESTAMP NOT NULL,
"block_hash" TEXT NOT NULL,
"metadata" JSONB NOT NULL
);
INSERT INTO events("tx_hash", "block_height", "tx_index",
"wallet_address", "valid", "action",
"raw_message", "parsed_message", "block_timestamp",
"block_hash", "metadata")
VALUES ('', -1, -1,
'', false, -1,
'', '{}', NOW(),
'', '{}');
CREATE TABLE IF NOT EXISTS node_sales (
"block_height" INTEGER NOT NULL,
"tx_index" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"starts_at" TIMESTAMP NOT NULL,
"ends_at" TIMESTAMP NOT NULL,
"tiers" JSONB[] NOT NULL,
"seller_public_key" TEXT NOT NULL,
"max_per_address" INTEGER NOT NULL,
"deploy_tx_hash" TEXT NOT NULL REFERENCES events(tx_hash) ON DELETE CASCADE,
"max_discount_percentage" INTEGER NOT NULL,
"seller_wallet" TEXT NOT NULL,
PRIMARY KEY ("block_height", "tx_index")
);
CREATE TABLE IF NOT EXISTS nodes (
"sale_block" INTEGER NOT NULL,
"sale_tx_index" INTEGER NOT NULL,
"node_id" INTEGER NOT NULL,
"tier_index" INTEGER NOT NULL,
"delegated_to" TEXT NOT NULL DEFAULT '',
"owner_public_key" TEXT NOT NULL,
"purchase_tx_hash" TEXT NOT NULL REFERENCES events(tx_hash) ON DELETE CASCADE,
"delegate_tx_hash" TEXT NOT NULL DEFAULT '' REFERENCES events(tx_hash) ON DELETE SET DEFAULT,
PRIMARY KEY("sale_block", "sale_tx_index", "node_id"),
FOREIGN KEY("sale_block", "sale_tx_index") REFERENCES node_sales("block_height", "tx_index")
);
COMMIT;

View File

@@ -1,16 +0,0 @@
-- name: GetLastProcessedBlock :one
SELECT * FROM blocks
WHERE "block_height" = (SELECT MAX("block_height") FROM blocks);
-- name: GetBlock :one
SELECT * FROM blocks
WHERE "block_height" = $1;
-- name: RemoveBlockFrom :execrows
DELETE FROM blocks
WHERE "block_height" >= @from_block;
-- name: AddBlock :exec
INSERT INTO blocks("block_height", "block_hash", "module")
VALUES ($1, $2, $3);

View File

@@ -1,13 +0,0 @@
-- name: RemoveEventsFromBlock :execrows
DELETE FROM events
WHERE "block_height" >= @from_block;
-- name: AddEvent :exec
INSERT INTO events("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
-- name: GetEventsByWallet :many
SELECT *
FROM events
WHERE wallet_address = $1;

View File

@@ -1,51 +0,0 @@
-- name: ClearDelegate :execrows
UPDATE nodes
SET "delegated_to" = ''
WHERE "delegate_tx_hash" = NULL;
-- name: SetDelegates :execrows
UPDATE nodes
SET delegated_to = @delegatee
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
node_id = ANY (@node_ids::int[]);
-- name: GetNodes :many
SELECT *
FROM nodes
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
node_id = ANY (@node_ids::int[]);
-- name: GetNodesByOwner :many
SELECT *
FROM nodes
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
owner_public_key = $3
ORDER BY tier_index;
-- name: GetNodesByPubkey :many
SELECT *
FROM nodes JOIN events ON nodes.purchase_tx_hash = events.tx_hash
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
owner_public_key = $3 AND
delegated_to = $4;
-- name: AddNode :exec
INSERT INTO nodes(sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
-- name: GetNodeCountByTierIndex :many
SELECT tiers.tier_index as tier_index, count(nodes.tier_index)
FROM generate_series(@from_tier::int,@to_tier::int) as tiers(tier_index)
LEFT JOIN
(select *
from nodes
where sale_block = $1 and
sale_tx_index= $2)
as nodes on tiers.tier_index = nodes.tier_index
group by tiers.tier_index
ORDER BY tiers.tier_index;

View File

@@ -1,9 +0,0 @@
-- name: AddNodesale :exec
INSERT INTO node_sales("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
-- name: GetNodesale :many
SELECT *
FROM node_sales
WHERE block_height = $1 AND
tx_index = $2;

View File

@@ -1,4 +0,0 @@
-- name: ClearEvents :exec
DELETE FROM events
WHERE tx_hash <> '';
;

View File

@@ -1,83 +0,0 @@
package nodesale
import (
"context"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5/pgtype"
)
func (p *Processor) processDelegate(ctx context.Context, qtx gen.Querier, block *types.Block, event nodesaleEvent) error {
valid := true
delegate := event.eventMessage.Delegate
nodeIds := make([]int32, len(delegate.NodeIDs))
for i, id := range delegate.NodeIDs {
nodeIds[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(delegate.DeployID.Block),
SaleTxIndex: int32(delegate.DeployID.TxIndex),
NodeIds: nodeIds,
})
if err != nil {
return fmt.Errorf("Failed to get nodes : %w", err)
}
if len(nodeIds) != len(nodes) {
valid = false
}
if valid {
for _, node := range nodes {
OwnerPublicKeyBytes, err := hex.DecodeString(node.OwnerPublicKey)
if err != nil {
valid = false
break
}
OwnerPublicKey, err := btcec.ParsePubKey(OwnerPublicKeyBytes)
if err != nil {
valid = false
break
}
if !event.txPubkey.IsEqual(OwnerPublicKey) {
valid = false
break
}
}
}
err = qtx.AddEvent(ctx, gen.AddEventParams{
TxHash: event.transaction.TxHash.String(),
TxIndex: int32(event.transaction.Index),
Action: int32(event.eventMessage.Action),
RawMessage: event.rawData,
ParsedMessage: event.eventJson,
BlockTimestamp: pgtype.Timestamp{Time: block.Header.Timestamp, Valid: true},
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: int32(event.transaction.BlockHeight),
Valid: valid,
// WalletAddress: event.txAddress.EncodeAddress(),
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: []byte("{}"),
})
if err != nil {
return fmt.Errorf("Failed to insert event : %w", err)
}
if valid {
_, err = qtx.SetDelegates(ctx, gen.SetDelegatesParams{
SaleBlock: int32(delegate.DeployID.Block),
SaleTxIndex: int32(delegate.DeployID.TxIndex),
Delegatee: delegate.DelegateePublicKey,
NodeIds: nodeIds,
})
if err != nil {
return fmt.Errorf("Failed to set delegate : %w", err)
}
}
return nil
}

View File

@@ -1,131 +0,0 @@
package nodesale
import (
"encoding/hex"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func TestDelegate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 4,
MaxPerAddress: 2,
},
{
PriceSat: 400,
Limit: 50,
MaxPerAddress: 3,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "111111", "111111", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
payload := &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{9, 10, 11},
TotalAmountSat: 600,
}
payloadBytes, _ := proto.Marshal(payload)
payloadHash := chainhash.DoubleHashB(payloadBytes)
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex := hex.EncodeToString(signature.Serialize())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "1212121212", "1212121212", 0, 0, message)
addr, _ := btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ := txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 600,
},
}
p.processPurchase(ctx, qtx, block, event)
delegateePrivateKey, _ := btcec.NewPrivateKey()
delegateePubkeyHex := hex.EncodeToString(delegateePrivateKey.PubKey().SerializeCompressed())
delegateMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DELEGATE,
Delegate: &protobuf.ActionDelegate{
DelegateePublicKey: delegateePubkeyHex,
NodeIDs: []uint32{9, 10},
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 2,
TxIndex: uint32(testTxIndex) - 2,
},
},
}
event, block = assembleTestEvent(buyerPrivateKey, "131313131313", "131313131313", 0, 0, delegateMessage)
p.processDelegate(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 3,
SaleTxIndex: int32(testTxIndex) - 3,
NodeIds: []int32{9, 10, 11},
})
require.Len(t, nodes, 3)
for _, node := range nodes {
if node.NodeID == 9 || node.NodeID == 10 {
require.NotEmpty(t, node.DelegatedTo)
} else if node.NodeID == 11 {
require.Empty(t, node.DelegatedTo)
} else {
require.Fail(t, "Unhandled")
}
}
}

View File

@@ -1,86 +0,0 @@
package nodesale
import (
"context"
"encoding/hex"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5/pgtype"
"google.golang.org/protobuf/encoding/protojson"
)
func (p *Processor) processDeploy(ctx context.Context, qtx gen.Querier, block *types.Block, event nodesaleEvent) error {
valid := true
deploy := event.eventMessage.Deploy
sellerPubKeyBytes, err := hex.DecodeString(deploy.SellerPublicKey)
if err != nil {
valid = false
}
if valid {
sellerPubKey, err := btcec.ParsePubKey(sellerPubKeyBytes)
if err != nil {
valid = false
}
if valid && !event.txPubkey.IsEqual(sellerPubKey) {
valid = false
}
}
tiers := make([][]byte, len(deploy.Tiers))
for i, tier := range deploy.Tiers {
tierJson, err := protojson.Marshal(tier)
if err != nil {
return fmt.Errorf("Failed to parse tiers to json : %w", err)
}
tiers[i] = tierJson
}
err = qtx.AddEvent(ctx, gen.AddEventParams{
TxHash: event.transaction.TxHash.String(),
TxIndex: int32(event.transaction.Index),
Action: int32(event.eventMessage.Action),
RawMessage: event.rawData,
ParsedMessage: event.eventJson,
BlockTimestamp: pgtype.Timestamp{Time: block.Header.Timestamp, Valid: true},
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: int32(event.transaction.BlockHeight),
Valid: valid,
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: []byte("{}"),
})
if err != nil {
return fmt.Errorf("Failed to insert event : %w", err)
}
if valid {
err = qtx.AddNodesale(ctx, gen.AddNodesaleParams{
BlockHeight: int32(event.transaction.BlockHeight),
TxIndex: int32(event.transaction.Index),
Name: deploy.Name,
StartsAt: pgtype.Timestamp{
Time: time.Unix(int64(deploy.StartsAt), 0).UTC(),
Valid: true,
},
EndsAt: pgtype.Timestamp{
Time: time.Unix(int64(deploy.EndsAt), 0).UTC(),
Valid: true,
},
Tiers: tiers,
SellerPublicKey: deploy.SellerPublicKey,
MaxPerAddress: int32(deploy.MaxPerAddress),
DeployTxHash: event.transaction.TxHash.String(),
MaxDiscountPercentage: int32(deploy.MaxDiscountPercentage),
SellerWallet: deploy.SellerWallet,
})
if err != nil {
return fmt.Errorf("Failed to insert nodesale : %w", err)
}
}
return nil
}

View File

@@ -1,94 +0,0 @@
package nodesale
import (
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/stretchr/testify/require"
)
func TestDeployInvalid(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
prvKey, _ := btcec.NewPrivateKey()
sellerWallet := p.pubkeyToPkHashAddress(prvKey.PubKey())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: 100,
EndsAt: 200,
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: "0102030405",
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(prvKey, "0101010101", "0101010101", 0, 0, message)
p.processDeploy(ctx, qtx, block, event)
nodesales, _ := qtx.GetNodesale(ctx, gen.GetNodesaleParams{
BlockHeight: int32(testBlockHeigh) - 1,
TxIndex: int32(testTxIndex) - 1,
})
require.Len(t, nodesales, 0)
}
func TestDeployValid(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
privateKey, _ := btcec.NewPrivateKey()
pubkeyHex := hex.EncodeToString(privateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(privateKey.PubKey())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: 100,
EndsAt: 200,
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: pubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(privateKey, "0202020202", "0202020202", 0, 0, message)
p.processDeploy(ctx, qtx, block, event)
nodesales, _ := qtx.GetNodesale(ctx, gen.GetNodesaleParams{
BlockHeight: int32(testBlockHeigh) - 1,
TxIndex: int32(testTxIndex) - 1,
})
require.Len(t, nodesales, 1)
}

View File

@@ -1,61 +0,0 @@
package nodesale
import (
"context"
"fmt"
"github.com/btcsuite/btcd/rpcclient"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/api/httphandler"
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gofiber/fiber/v2"
"github.com/samber/do/v2"
)
var NODESALE_MAGIC = []byte{0x63, 0x73, 0x6f, 0x70}
const (
NODESALE_LASTBLOCK_DEFAULT = 846851
Version = "v0.0.1-alpha"
)
func New(injector do.Injector) (indexer.IndexerWorker, error) {
ctx := do.MustInvoke[context.Context](injector)
conf := do.MustInvoke[config.Config](injector)
btcClient := do.MustInvoke[*rpcclient.Client](injector)
datasource := datasources.NewBitcoinNode(btcClient)
pg, err := postgres.NewPool(ctx, conf.Modules.Nodesale.Postgres)
if err != nil {
return nil, fmt.Errorf("Can't create postgres connection : %w", err)
}
var cleanupFuncs []func(context.Context) error
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
pg.Close()
return nil
})
repository := repository.NewRepository(pg)
processor := &Processor{
repository: repository,
btcClient: datasource,
network: conf.Network,
cleanupFuncs: cleanupFuncs,
}
httpServer := do.MustInvoke[*fiber.App](injector)
nodesaleHandler := httphandler.New(repository)
if err := nodesaleHandler.Mount(httpServer); err != nil {
return nil, fmt.Errorf("Can't mount nodesale API : %w", err)
}
logger.InfoContext(ctx, "Mounted nodesale HTTP handler")
indexer := indexer.New(processor, datasource)
logger.InfoContext(ctx, "Nodesale module started.")
return indexer, nil
}

View File

@@ -1,110 +0,0 @@
package nodesale
import (
"context"
"flag"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
var p *Processor
var postgresConf postgres.Config = postgres.Config{
User: "postgres",
Password: "P@ssw0rd",
}
var qtx gen.Querier
var ctx context.Context
var tx pgx.Tx
var (
testBlockHeigh int = 101
testTxIndex int = 1
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Short() {
return
}
ctx = context.Background()
db, _ := postgres.NewPool(ctx, postgresConf)
repo := repository.NewRepository(db)
p = &Processor{
repository: repo,
network: common.NetworkMainnet,
}
repo.Queries.ClearEvents(ctx)
tx, _ = p.repository.Db.Begin(ctx)
qtx = p.repository.WithTx(tx)
res := m.Run()
tx.Commit(ctx)
db.Close()
os.Exit(res)
}
func assembleTestEvent(privateKey *secp256k1.PrivateKey, blockHashHex, txHashHex string, blockHeight, txIndex int, message *protobuf.NodeSaleEvent) (nodesaleEvent, *types.Block) {
blockHash, _ := chainhash.NewHashFromStr(blockHashHex)
txHash, _ := chainhash.NewHashFromStr(txHashHex)
rawData, _ := proto.Marshal(message)
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_FALSE)
builder.AddOp(txscript.OP_IF)
builder.AddData(rawData)
builder.AddOp(txscript.OP_ENDIF)
messageJson, _ := protojson.Marshal(message)
if blockHeight == 0 {
blockHeight = testBlockHeigh
testBlockHeigh++
}
if txIndex == 0 {
txIndex = testTxIndex
testTxIndex++
}
event := nodesaleEvent{
transaction: &types.Transaction{
BlockHeight: int64(blockHeight),
BlockHash: *blockHash,
Index: uint32(txIndex),
TxHash: *txHash,
},
rawData: rawData,
eventMessage: message,
eventJson: messageJson,
txPubkey: privateKey.PubKey(),
}
block := &types.Block{
Header: types.BlockHeader{
Timestamp: time.Now().UTC(),
},
}
return event, block
}

View File

@@ -1,290 +0,0 @@
package nodesale
import (
"bytes"
"context"
"fmt"
"log/slog"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/pkg/logger"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
)
type Processor struct {
repository *repository.Repository
btcClient *datasources.BitcoinNodeDatasource
network common.Network
cleanupFuncs []func(context.Context) error
}
// CurrentBlock implements indexer.Processor.
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
block, err := p.repository.Queries.GetLastProcessedBlock(ctx)
if err != nil {
logger.InfoContext(ctx, "Couldn't get last processed block. Start from NODESALE_LAST_BLOCK_DEFAULT.",
slog.Int("currentBlock", NODESALE_LASTBLOCK_DEFAULT))
header, err := p.btcClient.GetBlockHeader(ctx, NODESALE_LASTBLOCK_DEFAULT)
if err != nil {
return types.BlockHeader{}, fmt.Errorf("Cannot get default block from bitcoin node : %w", err)
}
return types.BlockHeader{
Hash: header.Hash,
Height: NODESALE_LASTBLOCK_DEFAULT,
}, nil
}
hash, err := chainhash.NewHashFromStr(block.BlockHash)
if err != nil {
logger.PanicContext(ctx, "Invalid hash format found in Database.")
}
return types.BlockHeader{
Hash: *hash,
Height: int64(block.BlockHeight),
}, nil
}
// GetIndexedBlock implements indexer.Processor.
func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error) {
block, err := p.repository.Queries.GetBlock(ctx, int32(height))
if err != nil {
return types.BlockHeader{}, fmt.Errorf("Block %d not found : %w", height, err)
}
hash, err := chainhash.NewHashFromStr(block.BlockHash)
if err != nil {
logger.PanicContext(ctx, "Invalid hash format found in Database.")
}
return types.BlockHeader{
Hash: *hash,
Height: int64(block.BlockHeight),
}, nil
}
// Name implements indexer.Processor.
func (p *Processor) Name() string {
return "nodesale"
}
func extractNodesaleData(witness [][]byte) (data []byte, internalPubkey *btcec.PublicKey, isNodesale bool) {
tokenizer, controlBlock, isTapScript := extractTapScript(witness)
if !isTapScript {
return []byte{}, nil, false
}
state := 0
for tokenizer.Next() {
switch state {
case 0:
if tokenizer.Opcode() == txscript.OP_0 {
state++
} else {
state = 0
}
case 1:
if tokenizer.Opcode() == txscript.OP_IF {
state++
} else {
state = 0
}
case 2:
if tokenizer.Opcode() == txscript.OP_DATA_4 &&
bytes.Equal(tokenizer.Data(), NODESALE_MAGIC) {
state++
} else {
state = 0
}
case 3:
if tokenizer.Opcode() == txscript.OP_PUSHDATA1 {
data := tokenizer.Data()
return data, controlBlock.InternalKey, true
}
state = 0
}
}
return []byte{}, nil, false
}
type nodesaleEvent struct {
transaction *types.Transaction
eventMessage *protobuf.NodeSaleEvent
eventJson []byte
// txAddress btcutil.Address
txPubkey *btcec.PublicKey
rawData []byte
// rawScript []byte
}
func (p *Processor) parseTransactions(ctx context.Context, transactions []*types.Transaction) ([]nodesaleEvent, error) {
var events []nodesaleEvent
for _, t := range transactions {
for _, txIn := range t.TxIn {
data, txPubkey, isNodesale := extractNodesaleData(txIn.Witness)
if !isNodesale {
continue
}
event := &protobuf.NodeSaleEvent{}
err := proto.Unmarshal(data, event)
if err != nil {
logger.WarnContext(ctx, "Invalid Protobuf",
slog.String("block_hash", t.BlockHash.String()),
slog.Int("txIndex", int(t.Index)))
continue
}
eventJson, err := protojson.Marshal(event)
if err != nil {
return []nodesaleEvent{}, fmt.Errorf("Failed to parse protobuf to json : %w", err)
}
/*
outIndex := txIn.PreviousOutIndex
outHash := txIn.PreviousOutTxHash
result, err := p.btcClient.GetTransactionByHash(ctx, outHash)
if err != nil {
return []nodesaleEvent{}, fmt.Errorf("Failed to Get Bitcoin transaction : %w", err)
}
pkScript := result.TxOut[outIndex].PkScript
_, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, p.network.ChainParams())
if err != nil {
return []nodesaleEvent{}, fmt.Errorf("Failed to Get Bitcoin address : %w", err)
}
if len(addresses) != 1 {
return []nodesaleEvent{}, fmt.Errorf("Multiple addresses detected.")
}*/
events = append(events, nodesaleEvent{
transaction: t,
eventMessage: event,
eventJson: eventJson,
// txAddress: addresses[0],
rawData: data,
txPubkey: txPubkey,
// rawScript: rawScript,
})
}
}
return events, nil
}
// Process implements indexer.Processor.
func (p *Processor) Process(ctx context.Context, inputs []*types.Block) error {
for _, block := range inputs {
logger.InfoContext(ctx, "Nodesale processing a block",
slog.Int64("block", block.Header.Height),
slog.String("hash", block.Header.Hash.String()))
// parse all event from each transaction including reading tx wallet
events, err := p.parseTransactions(ctx, block.Transactions)
if err != nil {
return fmt.Errorf("Invalid data from bitcoin client : %w", err)
}
// open transaction
tx, err := p.repository.Db.Begin(ctx)
if err != nil {
return fmt.Errorf("Failed to create transaction : %w", err)
}
defer tx.Rollback(ctx)
qtx := p.repository.WithTx(tx)
// write block
err = qtx.AddBlock(ctx, gen.AddBlockParams{
BlockHeight: int32(block.Header.Height),
BlockHash: block.Header.Hash.String(),
Module: p.Name(),
})
if err != nil {
return fmt.Errorf("Failed to add block %d : %w", block.Header.Height, err)
}
// for each events
for _, event := range events {
logger.InfoContext(ctx, "Nodesale processing event",
slog.Int("txIndex", int(event.transaction.Index)),
slog.Int("blockHeight", int(block.Header.Height)),
slog.String("blockhash", block.Header.Hash.String()),
)
eventMessage := event.eventMessage
switch eventMessage.Action {
case protobuf.Action_ACTION_DEPLOY:
err = p.processDeploy(ctx, qtx, block, event)
if err != nil {
return fmt.Errorf("Failed to deploy at block %d : %w", block.Header.Height, err)
}
case protobuf.Action_ACTION_DELEGATE:
err = p.processDelegate(ctx, qtx, block, event)
if err != nil {
return fmt.Errorf("Failed to delegate at block %d : %w", block.Header.Height, err)
}
case protobuf.Action_ACTION_PURCHASE:
err = p.processPurchase(ctx, qtx, block, event)
if err != nil {
return fmt.Errorf("Failed to purchase at block %d : %w", block.Header.Height, err)
}
}
}
// close transaction
err = tx.Commit(ctx)
if err != nil {
return fmt.Errorf("Failed to commit transaction : %w", err)
}
logger.InfoContext(ctx, "Nodesale finished processing block",
slog.Int64("block", block.Header.Height),
slog.String("hash", block.Header.Hash.String()))
}
return nil
}
// RevertData implements indexer.Processor.
func (p *Processor) RevertData(ctx context.Context, from int64) error {
tx, err := p.repository.Db.Begin(ctx)
if err != nil {
return fmt.Errorf("Failed to create transaction : %w", err)
}
defer tx.Rollback(ctx)
qtx := p.repository.WithTx(tx)
_, err = qtx.RemoveBlockFrom(ctx, int32(from))
if err != nil {
return fmt.Errorf("Failed to remove blocks. : %w", err)
}
affected, err := qtx.RemoveEventsFromBlock(ctx, int32(from))
if err != nil {
return fmt.Errorf("Failed to remove events. : %w", err)
}
_, err = qtx.ClearDelegate(ctx)
if err != nil {
return fmt.Errorf("Failed to clear delegate from nodes : %w", err)
}
err = tx.Commit(ctx)
if err != nil {
return fmt.Errorf("Failed to commit transaction : %w", err)
}
logger.InfoContext(ctx, "Events removed",
slog.Int("Total removed", int(affected)))
return nil
}
// VerifyStates implements indexer.Processor.
func (p *Processor) VerifyStates(ctx context.Context) error {
panic("unimplemented")
}
func (p *Processor) Shutdown(ctx context.Context) error {
for _, cleanupFunc := range p.cleanupFuncs {
err := cleanupFunc(ctx)
if err != nil {
return fmt.Errorf("cleanup function error : %w", err)
}
}
return nil
}
var _ indexer.Processor[*types.Block] = (*Processor)(nil)

View File

@@ -1,7 +0,0 @@
package nodesale
import "testing"
func TestRevertData(t *testing.T) {
p.RevertData(ctx, 846855)
}

View File

@@ -1,806 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc v5.26.1
// source: modules/nodesale/protobuf/nodesale.proto
// protoc modules/nodesale/protobuf/nodesale.proto --go_out=. --go_opt=module=github.com/gaze-network/indexer-network
package protobuf
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Action int32
const (
Action_ACTION_DEPLOY Action = 0
Action_ACTION_PURCHASE Action = 1
Action_ACTION_DELEGATE Action = 2
)
// Enum value maps for Action.
var (
Action_name = map[int32]string{
0: "ACTION_DEPLOY",
1: "ACTION_PURCHASE",
2: "ACTION_DELEGATE",
}
Action_value = map[string]int32{
"ACTION_DEPLOY": 0,
"ACTION_PURCHASE": 1,
"ACTION_DELEGATE": 2,
}
)
func (x Action) Enum() *Action {
p := new(Action)
*p = x
return p
}
func (x Action) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Action) Descriptor() protoreflect.EnumDescriptor {
return file_modules_nodesale_protobuf_nodesale_proto_enumTypes[0].Descriptor()
}
func (Action) Type() protoreflect.EnumType {
return &file_modules_nodesale_protobuf_nodesale_proto_enumTypes[0]
}
func (x Action) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Action.Descriptor instead.
func (Action) EnumDescriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{0}
}
type NodeSaleEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Action Action `protobuf:"varint,1,opt,name=action,proto3,enum=nodesale.Action" json:"action,omitempty"`
Deploy *ActionDeploy `protobuf:"bytes,2,opt,name=deploy,proto3,oneof" json:"deploy,omitempty"`
Purchase *ActionPurchase `protobuf:"bytes,3,opt,name=purchase,proto3,oneof" json:"purchase,omitempty"`
Delegate *ActionDelegate `protobuf:"bytes,4,opt,name=delegate,proto3,oneof" json:"delegate,omitempty"`
}
func (x *NodeSaleEvent) Reset() {
*x = NodeSaleEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NodeSaleEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NodeSaleEvent) ProtoMessage() {}
func (x *NodeSaleEvent) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NodeSaleEvent.ProtoReflect.Descriptor instead.
func (*NodeSaleEvent) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{0}
}
func (x *NodeSaleEvent) GetAction() Action {
if x != nil {
return x.Action
}
return Action_ACTION_DEPLOY
}
func (x *NodeSaleEvent) GetDeploy() *ActionDeploy {
if x != nil {
return x.Deploy
}
return nil
}
func (x *NodeSaleEvent) GetPurchase() *ActionPurchase {
if x != nil {
return x.Purchase
}
return nil
}
func (x *NodeSaleEvent) GetDelegate() *ActionDelegate {
if x != nil {
return x.Delegate
}
return nil
}
type ActionDeploy struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
StartsAt uint32 `protobuf:"varint,2,opt,name=startsAt,proto3" json:"startsAt,omitempty"`
EndsAt uint32 `protobuf:"varint,3,opt,name=endsAt,proto3" json:"endsAt,omitempty"`
Tiers []*Tier `protobuf:"bytes,4,rep,name=tiers,proto3" json:"tiers,omitempty"`
SellerPublicKey string `protobuf:"bytes,5,opt,name=sellerPublicKey,proto3" json:"sellerPublicKey,omitempty"`
MaxPerAddress uint32 `protobuf:"varint,6,opt,name=maxPerAddress,proto3" json:"maxPerAddress,omitempty"`
MaxDiscountPercentage uint32 `protobuf:"varint,7,opt,name=maxDiscountPercentage,proto3" json:"maxDiscountPercentage,omitempty"`
SellerWallet string `protobuf:"bytes,8,opt,name=sellerWallet,proto3" json:"sellerWallet,omitempty"`
}
func (x *ActionDeploy) Reset() {
*x = ActionDeploy{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActionDeploy) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActionDeploy) ProtoMessage() {}
func (x *ActionDeploy) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActionDeploy.ProtoReflect.Descriptor instead.
func (*ActionDeploy) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{1}
}
func (x *ActionDeploy) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ActionDeploy) GetStartsAt() uint32 {
if x != nil {
return x.StartsAt
}
return 0
}
func (x *ActionDeploy) GetEndsAt() uint32 {
if x != nil {
return x.EndsAt
}
return 0
}
func (x *ActionDeploy) GetTiers() []*Tier {
if x != nil {
return x.Tiers
}
return nil
}
func (x *ActionDeploy) GetSellerPublicKey() string {
if x != nil {
return x.SellerPublicKey
}
return ""
}
func (x *ActionDeploy) GetMaxPerAddress() uint32 {
if x != nil {
return x.MaxPerAddress
}
return 0
}
func (x *ActionDeploy) GetMaxDiscountPercentage() uint32 {
if x != nil {
return x.MaxDiscountPercentage
}
return 0
}
func (x *ActionDeploy) GetSellerWallet() string {
if x != nil {
return x.SellerWallet
}
return ""
}
type Tier struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PriceSat uint32 `protobuf:"varint,1,opt,name=priceSat,proto3" json:"priceSat,omitempty"`
Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
MaxPerAddress uint32 `protobuf:"varint,3,opt,name=maxPerAddress,proto3" json:"maxPerAddress,omitempty"`
}
func (x *Tier) Reset() {
*x = Tier{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Tier) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Tier) ProtoMessage() {}
func (x *Tier) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Tier.ProtoReflect.Descriptor instead.
func (*Tier) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{2}
}
func (x *Tier) GetPriceSat() uint32 {
if x != nil {
return x.PriceSat
}
return 0
}
func (x *Tier) GetLimit() uint32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *Tier) GetMaxPerAddress() uint32 {
if x != nil {
return x.MaxPerAddress
}
return 0
}
type ActionPurchase struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Payload *PurchasePayload `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
SellerSignature string `protobuf:"bytes,2,opt,name=sellerSignature,proto3" json:"sellerSignature,omitempty"`
}
func (x *ActionPurchase) Reset() {
*x = ActionPurchase{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActionPurchase) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActionPurchase) ProtoMessage() {}
func (x *ActionPurchase) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActionPurchase.ProtoReflect.Descriptor instead.
func (*ActionPurchase) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{3}
}
func (x *ActionPurchase) GetPayload() *PurchasePayload {
if x != nil {
return x.Payload
}
return nil
}
func (x *ActionPurchase) GetSellerSignature() string {
if x != nil {
return x.SellerSignature
}
return ""
}
type PurchasePayload struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
DeployID *ActionID `protobuf:"bytes,1,opt,name=deployID,proto3" json:"deployID,omitempty"`
BuyerPublicKey string `protobuf:"bytes,2,opt,name=buyerPublicKey,proto3" json:"buyerPublicKey,omitempty"`
NodeIDs []uint32 `protobuf:"varint,3,rep,packed,name=nodeIDs,proto3" json:"nodeIDs,omitempty"`
TotalAmountSat int64 `protobuf:"varint,4,opt,name=totalAmountSat,proto3" json:"totalAmountSat,omitempty"`
TimeOutBlock uint64 `protobuf:"varint,5,opt,name=timeOutBlock,proto3" json:"timeOutBlock,omitempty"`
}
func (x *PurchasePayload) Reset() {
*x = PurchasePayload{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PurchasePayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PurchasePayload) ProtoMessage() {}
func (x *PurchasePayload) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PurchasePayload.ProtoReflect.Descriptor instead.
func (*PurchasePayload) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{4}
}
func (x *PurchasePayload) GetDeployID() *ActionID {
if x != nil {
return x.DeployID
}
return nil
}
func (x *PurchasePayload) GetBuyerPublicKey() string {
if x != nil {
return x.BuyerPublicKey
}
return ""
}
func (x *PurchasePayload) GetNodeIDs() []uint32 {
if x != nil {
return x.NodeIDs
}
return nil
}
func (x *PurchasePayload) GetTotalAmountSat() int64 {
if x != nil {
return x.TotalAmountSat
}
return 0
}
func (x *PurchasePayload) GetTimeOutBlock() uint64 {
if x != nil {
return x.TimeOutBlock
}
return 0
}
type ActionID struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Block uint64 `protobuf:"varint,1,opt,name=block,proto3" json:"block,omitempty"`
TxIndex uint32 `protobuf:"varint,2,opt,name=txIndex,proto3" json:"txIndex,omitempty"`
}
func (x *ActionID) Reset() {
*x = ActionID{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActionID) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActionID) ProtoMessage() {}
func (x *ActionID) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActionID.ProtoReflect.Descriptor instead.
func (*ActionID) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{5}
}
func (x *ActionID) GetBlock() uint64 {
if x != nil {
return x.Block
}
return 0
}
func (x *ActionID) GetTxIndex() uint32 {
if x != nil {
return x.TxIndex
}
return 0
}
type ActionDelegate struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
DelegateePublicKey string `protobuf:"bytes,1,opt,name=delegateePublicKey,proto3" json:"delegateePublicKey,omitempty"`
NodeIDs []uint32 `protobuf:"varint,2,rep,packed,name=nodeIDs,proto3" json:"nodeIDs,omitempty"`
DeployID *ActionID `protobuf:"bytes,3,opt,name=deployID,proto3" json:"deployID,omitempty"`
}
func (x *ActionDelegate) Reset() {
*x = ActionDelegate{}
if protoimpl.UnsafeEnabled {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActionDelegate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActionDelegate) ProtoMessage() {}
func (x *ActionDelegate) ProtoReflect() protoreflect.Message {
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActionDelegate.ProtoReflect.Descriptor instead.
func (*ActionDelegate) Descriptor() ([]byte, []int) {
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{6}
}
func (x *ActionDelegate) GetDelegateePublicKey() string {
if x != nil {
return x.DelegateePublicKey
}
return ""
}
func (x *ActionDelegate) GetNodeIDs() []uint32 {
if x != nil {
return x.NodeIDs
}
return nil
}
func (x *ActionDelegate) GetDeployID() *ActionID {
if x != nil {
return x.DeployID
}
return nil
}
var File_modules_nodesale_protobuf_nodesale_proto protoreflect.FileDescriptor
var file_modules_nodesale_protobuf_nodesale_proto_rawDesc = []byte{
0x0a, 0x28, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x6e, 0x6f, 0x64, 0x65,
0x73, 0x61, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6e, 0x6f, 0x64, 0x65,
0x73, 0x61, 0x6c, 0x65, 0x22, 0x89, 0x02, 0x0a, 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x61, 0x6c,
0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c,
0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x33, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x16, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, 0x70, 0x6c,
0x6f, 0x79, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73,
0x65, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x88, 0x01, 0x01,
0x12, 0x39, 0x0a, 0x08, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x48, 0x02, 0x52, 0x08,
0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f,
0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x75, 0x72, 0x63, 0x68,
0x61, 0x73, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65,
0x22, 0xa6, 0x02, 0x0a, 0x0c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f,
0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x41,
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x41,
0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x64, 0x73, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x06, 0x65, 0x6e, 0x64, 0x73, 0x41, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x74, 0x69, 0x65,
0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73,
0x61, 0x6c, 0x65, 0x2e, 0x54, 0x69, 0x65, 0x72, 0x52, 0x05, 0x74, 0x69, 0x65, 0x72, 0x73, 0x12,
0x28, 0x0a, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72,
0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x78,
0x50, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x0d, 0x6d, 0x61, 0x78, 0x50, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
0x34, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65,
0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15,
0x6d, 0x61, 0x78, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65,
0x6e, 0x74, 0x61, 0x67, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x57,
0x61, 0x6c, 0x6c, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x6c,
0x6c, 0x65, 0x72, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x22, 0x5e, 0x0a, 0x04, 0x54, 0x69, 0x65,
0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x63, 0x65, 0x53, 0x61, 0x74, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x69, 0x63, 0x65, 0x53, 0x61, 0x74, 0x12, 0x14, 0x0a,
0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69,
0x6d, 0x69, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x50, 0x65, 0x72, 0x41, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x50,
0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x6f, 0x0a, 0x0e, 0x41, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x70,
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6e,
0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65,
0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65,
0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xcf, 0x01, 0x0a, 0x0f, 0x50,
0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2e,
0x0a, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x49, 0x44, 0x52, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x44, 0x12, 0x26,
0x0a, 0x0e, 0x62, 0x75, 0x79, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x75, 0x79, 0x65, 0x72, 0x50, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44,
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x73,
0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53,
0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41,
0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65,
0x4f, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c,
0x74, 0x69, 0x6d, 0x65, 0x4f, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x3a, 0x0a, 0x08,
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63,
0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x18,
0x0a, 0x07, 0x74, 0x78, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x07, 0x74, 0x78, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x8a, 0x01, 0x0a, 0x0e, 0x41, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x64,
0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74,
0x65, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6e,
0x6f, 0x64, 0x65, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x6e, 0x6f,
0x64, 0x65, 0x49, 0x44, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49,
0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x08, 0x64, 0x65, 0x70,
0x6c, 0x6f, 0x79, 0x49, 0x44, 0x2a, 0x45, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
0x11, 0x0a, 0x0d, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59,
0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x55, 0x52,
0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x43, 0x54, 0x49, 0x4f,
0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x47, 0x41, 0x54, 0x45, 0x10, 0x02, 0x42, 0x43, 0x5a, 0x41,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x2d,
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x72, 0x2d,
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x2f,
0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_modules_nodesale_protobuf_nodesale_proto_rawDescOnce sync.Once
file_modules_nodesale_protobuf_nodesale_proto_rawDescData = file_modules_nodesale_protobuf_nodesale_proto_rawDesc
)
func file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP() []byte {
file_modules_nodesale_protobuf_nodesale_proto_rawDescOnce.Do(func() {
file_modules_nodesale_protobuf_nodesale_proto_rawDescData = protoimpl.X.CompressGZIP(file_modules_nodesale_protobuf_nodesale_proto_rawDescData)
})
return file_modules_nodesale_protobuf_nodesale_proto_rawDescData
}
var file_modules_nodesale_protobuf_nodesale_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_modules_nodesale_protobuf_nodesale_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_modules_nodesale_protobuf_nodesale_proto_goTypes = []interface{}{
(Action)(0), // 0: nodesale.Action
(*NodeSaleEvent)(nil), // 1: nodesale.NodeSaleEvent
(*ActionDeploy)(nil), // 2: nodesale.ActionDeploy
(*Tier)(nil), // 3: nodesale.Tier
(*ActionPurchase)(nil), // 4: nodesale.ActionPurchase
(*PurchasePayload)(nil), // 5: nodesale.PurchasePayload
(*ActionID)(nil), // 6: nodesale.ActionID
(*ActionDelegate)(nil), // 7: nodesale.ActionDelegate
}
var file_modules_nodesale_protobuf_nodesale_proto_depIdxs = []int32{
0, // 0: nodesale.NodeSaleEvent.action:type_name -> nodesale.Action
2, // 1: nodesale.NodeSaleEvent.deploy:type_name -> nodesale.ActionDeploy
4, // 2: nodesale.NodeSaleEvent.purchase:type_name -> nodesale.ActionPurchase
7, // 3: nodesale.NodeSaleEvent.delegate:type_name -> nodesale.ActionDelegate
3, // 4: nodesale.ActionDeploy.tiers:type_name -> nodesale.Tier
5, // 5: nodesale.ActionPurchase.payload:type_name -> nodesale.PurchasePayload
6, // 6: nodesale.PurchasePayload.deployID:type_name -> nodesale.ActionID
6, // 7: nodesale.ActionDelegate.deployID:type_name -> nodesale.ActionID
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_modules_nodesale_protobuf_nodesale_proto_init() }
func file_modules_nodesale_protobuf_nodesale_proto_init() {
if File_modules_nodesale_protobuf_nodesale_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NodeSaleEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActionDeploy); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Tier); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActionPurchase); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PurchasePayload); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActionID); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActionDelegate); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_modules_nodesale_protobuf_nodesale_proto_rawDesc,
NumEnums: 1,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_modules_nodesale_protobuf_nodesale_proto_goTypes,
DependencyIndexes: file_modules_nodesale_protobuf_nodesale_proto_depIdxs,
EnumInfos: file_modules_nodesale_protobuf_nodesale_proto_enumTypes,
MessageInfos: file_modules_nodesale_protobuf_nodesale_proto_msgTypes,
}.Build()
File_modules_nodesale_protobuf_nodesale_proto = out.File
file_modules_nodesale_protobuf_nodesale_proto_rawDesc = nil
file_modules_nodesale_protobuf_nodesale_proto_goTypes = nil
file_modules_nodesale_protobuf_nodesale_proto_depIdxs = nil
}

View File

@@ -1,60 +0,0 @@
syntax = "proto3";
// protoc modules/nodesale/protobuf/nodesale.proto --go_out=. --go_opt=module=github.com/gaze-network/indexer-network
package nodesale;
option go_package = "github.com/gaze-network/indexer-network/modules/nodesale/protobuf";
enum Action {
ACTION_DEPLOY = 0;
ACTION_PURCHASE = 1;
ACTION_DELEGATE = 2;
}
message NodeSaleEvent {
Action action = 1;
optional ActionDeploy deploy = 2;
optional ActionPurchase purchase = 3;
optional ActionDelegate delegate = 4;
}
message ActionDeploy {
string name = 1;
uint32 startsAt = 2;
uint32 endsAt = 3;
repeated Tier tiers = 4;
string sellerPublicKey = 5;
uint32 maxPerAddress = 6;
uint32 maxDiscountPercentage = 7;
string sellerWallet = 8;
}
message Tier {
uint32 priceSat = 1;
uint32 limit = 2;
uint32 maxPerAddress = 3;
}
message ActionPurchase {
PurchasePayload payload = 1;
string sellerSignature = 2;
}
message PurchasePayload {
ActionID deployID = 1;
string buyerPublicKey = 2;
repeated uint32 nodeIDs = 3;
int64 totalAmountSat = 4;
uint64 timeOutBlock = 5;
}
message ActionID {
uint64 block = 1;
uint32 txIndex = 2;
}
message ActionDelegate {
string delegateePublicKey = 1;
repeated uint32 nodeIDs = 2;
ActionID deployID = 3;
}

View File

@@ -1,39 +0,0 @@
package nodesale
import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
)
/*
func (p *Processor) pubkeyToTaprootAddress(pubkey string, script []byte) (btcutil.Address, error) {
pubKeyBytes, err := hex.DecodeString(pubkey)
if err != nil {
return nil, fmt.Errorf("Failed to decode string : %w", err)
}
pubKey, err := btcec.ParsePubKey(pubKeyBytes)
if err != nil {
return nil, fmt.Errorf("Failed to parse pubkey : %w", err)
}
tapleaf := txscript.NewBaseTapLeaf(script)
scriptTree := txscript.AssembleTaprootScriptTree(tapleaf)
rootHash := scriptTree.RootNode.TapHash()
tapkey := txscript.ComputeTaprootOutputKey(pubKey, rootHash[:])
sellerAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(tapkey), p.network.ChainParams())
if err != nil {
return nil, fmt.Errorf("invalid taproot address: %w", err)
}
return sellerAddr, nil
}*/
func (p *Processor) pubkeyToPkHashAddress(pubKey *btcec.PublicKey) btcutil.Address {
// pubKeyBytes, _ := hex.DecodeString(pubkey)
// pubKey, _ := btcec.ParsePubKey(pubKeyBytes)
addrPubKey, _ := btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), p.network.ChainParams())
addrPubKeyHash := addrPubKey.AddressPubKeyHash()
return addrPubKeyHash
}

View File

@@ -1,270 +0,0 @@
package nodesale
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"slices"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5/pgtype"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type metaData struct {
ExpectedTotalAmountDiscounted int64
ReportedTotalAmount int64
PaidTotalAmount int64
}
func (p *Processor) processPurchase(ctx context.Context, qtx gen.Querier, block *types.Block, event nodesaleEvent) error {
valid := true
purchase := event.eventMessage.Purchase
payload := purchase.Payload
buyerPubkeyBytes, err := hex.DecodeString(payload.BuyerPublicKey)
if err != nil {
valid = false
}
if valid {
buyerPubkey, err := btcec.ParsePubKey(buyerPubkeyBytes)
if err != nil {
valid = false
}
if valid && !event.txPubkey.IsEqual(buyerPubkey) {
valid = false
}
}
var deploy *gen.NodeSale
if valid {
// check node existed
deploys, err := qtx.GetNodesale(ctx, gen.GetNodesaleParams{
BlockHeight: int32(payload.DeployID.Block),
TxIndex: int32(payload.DeployID.TxIndex),
})
if err != nil {
return fmt.Errorf("Failed to Get nodesale : %w", err)
}
if len(deploys) < 1 {
valid = false
} else {
deploy = &deploys[0]
}
}
if valid {
// check timestamp
timestamp := block.Header.Timestamp
if timestamp.UTC().Before(deploy.StartsAt.Time.UTC()) ||
timestamp.UTC().After(deploy.EndsAt.Time.UTC()) {
valid = false
}
}
if valid {
if payload.TimeOutBlock < uint64(event.transaction.BlockHeight) {
valid = false
}
}
if valid {
// verified signature
payloadBytes, _ := proto.Marshal(payload)
signatureBytes, _ := hex.DecodeString(purchase.SellerSignature)
signature, err := ecdsa.ParseSignature(signatureBytes)
if err != nil {
valid = false
}
if valid {
hash := chainhash.DoubleHashB(payloadBytes)
pubkeyBytes, _ := hex.DecodeString(deploy.SellerPublicKey)
pubKey, _ := btcec.ParsePubKey(pubkeyBytes)
verified := signature.Verify(hash[:], pubKey)
if !verified {
valid = false
}
}
}
var tiers []protobuf.Tier
var buyingTiersCount []uint32
nodeIdToTier := make(map[uint32]int32, 1)
if valid {
// valid nodeID tier
tiers = make([]protobuf.Tier, len(deploy.Tiers))
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
return fmt.Errorf("Failed to decode tiers json : %w", err)
}
}
slices.Sort(payload.NodeIDs)
buyingTiersCount = make([]uint32, len(tiers))
var currentTier int32 = -1
var tierSum uint32 = 0
for _, nodeId := range payload.NodeIDs {
for nodeId >= tierSum && currentTier < int32(len(tiers)-1) {
currentTier++
tierSum += tiers[currentTier].Limit
}
if nodeId < tierSum {
buyingTiersCount[currentTier]++
nodeIdToTier[nodeId] = currentTier
} else {
valid = false
}
}
}
if valid {
// valid unpurchased node ID
nodeIds := make([]int32, len(payload.NodeIDs))
for i, id := range payload.NodeIDs {
nodeIds[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(payload.DeployID.Block),
SaleTxIndex: int32(payload.DeployID.TxIndex),
NodeIds: nodeIds,
})
if err != nil {
return fmt.Errorf("Failed to Get nodes : %w", err)
}
if len(nodes) > 0 {
valid = false
}
}
var sellerAddr btcutil.Address
if valid {
sellerAddr, err = btcutil.DecodeAddress(deploy.SellerWallet, p.network.ChainParams())
if err != nil {
valid = false
}
}
var txPaid int64 = 0
meta := metaData{}
if valid {
// get total amount paid to seller
for _, txOut := range event.transaction.TxOut {
_, txOutAddrs, _, _ := txscript.ExtractPkScriptAddrs(txOut.PkScript, p.network.ChainParams())
if len(txOutAddrs) == 1 && bytes.Equal(
[]byte(sellerAddr.EncodeAddress()),
[]byte(txOutAddrs[0].EncodeAddress()),
) {
txPaid += txOut.Value
}
}
meta.PaidTotalAmount = txPaid
meta.ReportedTotalAmount = payload.TotalAmountSat
// total amount paid is greater than report paid
if txPaid < payload.TotalAmountSat {
valid = false
}
// calculate total price
var totalPrice int64 = 0
for i := 0; i < len(tiers); i++ {
totalPrice += int64(buyingTiersCount[i] * tiers[i].PriceSat)
}
// report paid is greater than max discounted total price
maxDiscounted := totalPrice * (100 - int64(deploy.MaxDiscountPercentage))
decimal := maxDiscounted % 100
maxDiscounted /= 100
if decimal%100 >= 50 {
maxDiscounted++
}
meta.ExpectedTotalAmountDiscounted = maxDiscounted
if payload.TotalAmountSat < maxDiscounted {
valid = false
}
}
var buyerOwnedNodes []gen.Node
if valid {
var err error
// check node limit
// get all selled by seller and owned by buyer
buyerOwnedNodes, err = qtx.GetNodesByOwner(ctx, gen.GetNodesByOwnerParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
OwnerPublicKey: payload.BuyerPublicKey,
})
if err != nil {
return fmt.Errorf("Failed to GetNodesByOwner : %w", err)
}
if len(buyerOwnedNodes)+len(payload.NodeIDs) > int(deploy.MaxPerAddress) {
valid = false
}
}
if valid {
// check limit
// count each tiers
// check limited for each tier
ownedTiersCount := make([]uint32, len(tiers))
for _, node := range buyerOwnedNodes {
ownedTiersCount[node.TierIndex]++
}
for i := 0; i < len(tiers); i++ {
if ownedTiersCount[i]+buyingTiersCount[i] > tiers[i].MaxPerAddress {
valid = false
break
}
}
}
metaDataBytes, _ := json.Marshal(meta)
err = qtx.AddEvent(ctx, gen.AddEventParams{
TxHash: event.transaction.TxHash.String(),
TxIndex: int32(event.transaction.Index),
Action: int32(event.eventMessage.Action),
RawMessage: event.rawData,
ParsedMessage: event.eventJson,
BlockTimestamp: pgtype.Timestamp{Time: block.Header.Timestamp, Valid: true},
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: int32(event.transaction.BlockHeight),
Valid: valid,
// WalletAddress: event.txAddress.EncodeAddress(),
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: metaDataBytes,
})
if err != nil {
return fmt.Errorf("Failed to insert event : %w", err)
}
if valid {
// add to node
for _, nodeId := range payload.NodeIDs {
err := qtx.AddNode(ctx, gen.AddNodeParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
NodeID: int32(nodeId),
TierIndex: nodeIdToTier[nodeId],
DelegatedTo: "",
OwnerPublicKey: payload.BuyerPublicKey,
PurchaseTxHash: event.transaction.TxHash.String(),
DelegateTxHash: "",
})
if err != nil {
return fmt.Errorf("Failed to insert node : %w", err)
}
}
}
return nil
}

View File

@@ -1,686 +0,0 @@
package nodesale
import (
"encoding/hex"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func TestInvalidPurchase(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: 111,
TxIndex: 1,
},
NodeIDs: []uint32{1, 2},
BuyerPublicKey: buyerPubkeyHex,
TotalAmountSat: 500,
TimeOutBlock: uint64(testBlockHeigh) + 5,
},
},
}
event, block := assembleTestEvent(buyerPrivateKey, "030303030303", "030303030303", 0, 0, message)
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: 111,
SaleTxIndex: 1,
NodeIds: []int32{1, 2},
})
require.Len(t, nodes, 0)
}
func TestInvalidTimestamp(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "040404040404", "040404040404", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
NodeIDs: []uint32{1, 2},
BuyerPublicKey: buyerPubkeyHex,
TotalAmountSat: 200,
TimeOutBlock: uint64(testBlockHeigh) + 5,
},
},
}
event, block = assembleTestEvent(buyerPrivateKey, "050505050505", "050505050505", 0, 0, message)
block.Header.Timestamp = time.Now().UTC().Add(time.Hour * 2)
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 2,
SaleTxIndex: int32(testTxIndex) - 2,
NodeIds: []int32{1, 2},
})
require.Len(t, nodes, 0)
}
func TestInvalidBuyerKey(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "060606060606", "060606060606", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
NodeIDs: []uint32{1, 2},
BuyerPublicKey: sellerPubkeyHex,
TotalAmountSat: 200,
TimeOutBlock: uint64(testBlockHeigh) + 5,
},
},
}
event, block = assembleTestEvent(buyerPrivateKey, "0707070707", "0707070707", 0, 0, message)
block.Header.Timestamp = time.Now().UTC().Add(time.Hour * 2)
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 2,
SaleTxIndex: int32(testTxIndex) - 2,
NodeIds: []int32{1, 2},
})
require.Len(t, nodes, 0)
}
func TestTimeOut(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "0808080808", "0808080808", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
NodeIDs: []uint32{1, 2},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) - 5,
TotalAmountSat: 200,
},
},
}
event, block = assembleTestEvent(buyerPrivateKey, "090909090909", "090909090909", 0, 0, message)
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 2,
SaleTxIndex: int32(testTxIndex) - 2,
NodeIds: []int32{1, 2},
})
require.Len(t, nodes, 0)
}
func TestSignatureInvalid(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 5,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "0A0A0A0A", "0A0A0A0A", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
payload := &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
NodeIDs: []uint32{1, 2},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
}
payloadBytes, _ := proto.Marshal(payload)
payloadHash := chainhash.DoubleHashB(payloadBytes)
signature := ecdsa.Sign(buyerPrivateKey, payloadHash[:])
signatureHex := hex.EncodeToString(signature.Serialize())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "0B0B0B", "0B0B0B", 0, 0, message)
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 2,
SaleTxIndex: int32(testTxIndex) - 2,
NodeIds: []int32{1, 2},
})
require.Len(t, nodes, 0)
}
func TestValidPurchase(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 4,
MaxPerAddress: 2,
},
{
PriceSat: 400,
Limit: 3,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "0C0C0C0C0C", "0C0C0C0C0C", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
payload := &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{0, 5, 6, 9},
TotalAmountSat: 500,
}
payloadBytes, _ := proto.Marshal(payload)
payloadHash := chainhash.DoubleHashB(payloadBytes)
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex := hex.EncodeToString(signature.Serialize())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "0D0D0D0D", "0D0D0D0D", 0, 0, message)
addr, _ := btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ := txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 500,
},
}
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 2,
SaleTxIndex: int32(testTxIndex) - 2,
NodeIds: []int32{0, 5, 6, 9},
})
require.Len(t, nodes, 4)
ids := make([]int32, len(nodes))
for i, id := range nodes {
ids[i] = id.NodeID
}
require.Contains(t, ids, int32(0))
require.Contains(t, ids, int32(5))
require.Contains(t, ids, int32(6))
require.Contains(t, ids, int32(9))
}
func TestBuyingLimit(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 4,
MaxPerAddress: 2,
},
{
PriceSat: 400,
Limit: 50,
MaxPerAddress: 100,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 2,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "2121212121", "2121212121", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
payload := &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{9, 10},
TotalAmountSat: 600,
}
payloadBytes, _ := proto.Marshal(payload)
payloadHash := chainhash.DoubleHashB(payloadBytes)
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex := hex.EncodeToString(signature.Serialize())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "2020202020", "2020202020", 0, 0, message)
addr, _ := btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ := txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 600,
},
}
p.processPurchase(ctx, qtx, block, event)
tx.Commit(ctx)
tx, _ = p.repository.Db.Begin(ctx)
qtx = p.repository.WithTx(tx)
payload = &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 2,
TxIndex: uint32(testTxIndex) - 2,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{11},
TotalAmountSat: 600,
}
payloadBytes, _ = proto.Marshal(payload)
payloadHash = chainhash.DoubleHashB(payloadBytes)
signature = ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex = hex.EncodeToString(signature.Serialize())
message = &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "22222222", "22222222", 0, 0, message)
addr, _ = btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ = txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 600,
},
}
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 3,
SaleTxIndex: int32(testTxIndex) - 3,
NodeIds: []int32{9, 10, 11},
})
require.Len(t, nodes, 2)
ids := make([]int32, len(nodes))
for i, id := range nodes {
ids[i] = id.NodeID
}
require.Contains(t, ids, int32(9))
require.Contains(t, ids, int32(10))
}
func TestBuyingTierLimit(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
sellerPrivateKey, _ := btcec.NewPrivateKey()
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(sellerPrivateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
deployMessage := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
Limit: 5,
MaxPerAddress: 100,
},
{
PriceSat: 200,
Limit: 4,
MaxPerAddress: 2,
},
{
PriceSat: 400,
Limit: 50,
MaxPerAddress: 3,
},
},
SellerPublicKey: sellerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
},
}
event, block := assembleTestEvent(sellerPrivateKey, "0E0E0E0E", "0E0E0E0E", 0, 0, deployMessage)
p.processDeploy(ctx, qtx, block, event)
buyerPrivateKey, _ := btcec.NewPrivateKey()
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
payload := &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 1,
TxIndex: uint32(testTxIndex) - 1,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{9, 10, 11},
TotalAmountSat: 600,
}
payloadBytes, _ := proto.Marshal(payload)
payloadHash := chainhash.DoubleHashB(payloadBytes)
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex := hex.EncodeToString(signature.Serialize())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "0F0F0F0F0F", "0F0F0F0F0F", 0, 0, message)
addr, _ := btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ := txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 600,
},
}
p.processPurchase(ctx, qtx, block, event)
tx.Commit(ctx)
tx, _ = p.repository.Db.Begin(ctx)
qtx = p.repository.WithTx(tx)
payload = &protobuf.PurchasePayload{
DeployID: &protobuf.ActionID{
Block: uint64(testBlockHeigh) - 2,
TxIndex: uint32(testTxIndex) - 2,
},
BuyerPublicKey: buyerPubkeyHex,
TimeOutBlock: uint64(testBlockHeigh) + 5,
NodeIDs: []uint32{12, 13, 14},
TotalAmountSat: 600,
}
payloadBytes, _ = proto.Marshal(payload)
payloadHash = chainhash.DoubleHashB(payloadBytes)
signature = ecdsa.Sign(sellerPrivateKey, payloadHash[:])
signatureHex = hex.EncodeToString(signature.Serialize())
message = &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_PURCHASE,
Purchase: &protobuf.ActionPurchase{
Payload: payload,
SellerSignature: signatureHex,
},
}
event, block = assembleTestEvent(buyerPrivateKey, "10101010", "10101010", 0, 0, message)
addr, _ = btcutil.NewAddressPubKey(sellerPrivateKey.PubKey().SerializeCompressed(), p.network.ChainParams())
pkscript, _ = txscript.PayToAddrScript(addr.AddressPubKeyHash())
event.transaction.TxOut = []*types.TxOut{
{
PkScript: pkscript,
Value: 600,
},
}
p.processPurchase(ctx, qtx, block, event)
nodes, _ := qtx.GetNodes(ctx, gen.GetNodesParams{
SaleBlock: int32(testBlockHeigh) - 3,
SaleTxIndex: int32(testTxIndex) - 3,
NodeIds: []int32{9, 10, 11, 12, 13, 14},
})
require.Len(t, nodes, 3)
ids := make([]int32, len(nodes))
for i, id := range nodes {
ids[i] = id.NodeID
}
require.Contains(t, ids, int32(9))
require.Contains(t, ids, int32(10))
require.Contains(t, ids, int32(11))
}

View File

@@ -1,63 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: blocks.sql
package gen
import (
"context"
)
const addBlock = `-- name: AddBlock :exec
INSERT INTO blocks("block_height", "block_hash", "module")
VALUES ($1, $2, $3)
`
type AddBlockParams struct {
BlockHeight int32
BlockHash string
Module string
}
func (q *Queries) AddBlock(ctx context.Context, arg AddBlockParams) error {
_, err := q.db.Exec(ctx, addBlock, arg.BlockHeight, arg.BlockHash, arg.Module)
return err
}
const getBlock = `-- name: GetBlock :one
SELECT block_height, block_hash, module FROM blocks
WHERE "block_height" = $1
`
func (q *Queries) GetBlock(ctx context.Context, blockHeight int32) (Block, error) {
row := q.db.QueryRow(ctx, getBlock, blockHeight)
var i Block
err := row.Scan(&i.BlockHeight, &i.BlockHash, &i.Module)
return i, err
}
const getLastProcessedBlock = `-- name: GetLastProcessedBlock :one
SELECT block_height, block_hash, module FROM blocks
WHERE "block_height" = (SELECT MAX("block_height") FROM blocks)
`
func (q *Queries) GetLastProcessedBlock(ctx context.Context) (Block, error) {
row := q.db.QueryRow(ctx, getLastProcessedBlock)
var i Block
err := row.Scan(&i.BlockHeight, &i.BlockHash, &i.Module)
return i, err
}
const removeBlockFrom = `-- name: RemoveBlockFrom :execrows
DELETE FROM blocks
WHERE "block_height" >= $1
`
func (q *Queries) RemoveBlockFrom(ctx context.Context, fromBlock int32) (int64, error) {
result, err := q.db.Exec(ctx, removeBlockFrom, fromBlock)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -1,100 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: events.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const addEvent = `-- name: AddEvent :exec
INSERT INTO events("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type AddEventParams struct {
TxHash string
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
Action int32
RawMessage []byte
ParsedMessage []byte
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
}
func (q *Queries) AddEvent(ctx context.Context, arg AddEventParams) error {
_, err := q.db.Exec(ctx, addEvent,
arg.TxHash,
arg.BlockHeight,
arg.TxIndex,
arg.WalletAddress,
arg.Valid,
arg.Action,
arg.RawMessage,
arg.ParsedMessage,
arg.BlockTimestamp,
arg.BlockHash,
arg.Metadata,
)
return err
}
const getEventsByWallet = `-- name: GetEventsByWallet :many
SELECT tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata
FROM events
WHERE wallet_address = $1
`
func (q *Queries) GetEventsByWallet(ctx context.Context, walletAddress string) ([]Event, error) {
rows, err := q.db.Query(ctx, getEventsByWallet, walletAddress)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Event
for rows.Next() {
var i Event
if err := rows.Scan(
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.WalletAddress,
&i.Valid,
&i.Action,
&i.RawMessage,
&i.ParsedMessage,
&i.BlockTimestamp,
&i.BlockHash,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const removeEventsFromBlock = `-- name: RemoveEventsFromBlock :execrows
DELETE FROM events
WHERE "block_height" >= $1
`
func (q *Queries) RemoveEventsFromBlock(ctx context.Context, fromBlock int32) (int64, error) {
result, err := q.db.Exec(ctx, removeEventsFromBlock, fromBlock)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -1,54 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package gen
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Block struct {
BlockHeight int32
BlockHash string
Module string
}
type Event struct {
TxHash string
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
Action int32
RawMessage []byte
ParsedMessage []byte
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
}
type Node struct {
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
DelegatedTo string
OwnerPublicKey string
PurchaseTxHash string
DelegateTxHash string
}
type NodeSale struct {
BlockHeight int32
TxIndex int32
Name string
StartsAt pgtype.Timestamp
EndsAt pgtype.Timestamp
Tiers [][]byte
SellerPublicKey string
MaxPerAddress int32
DeployTxHash string
MaxDiscountPercentage int32
SellerWallet string
}

View File

@@ -1,304 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: nodes.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const addNode = `-- name: AddNode :exec
INSERT INTO nodes(sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
type AddNodeParams struct {
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
DelegatedTo string
OwnerPublicKey string
PurchaseTxHash string
DelegateTxHash string
}
func (q *Queries) AddNode(ctx context.Context, arg AddNodeParams) error {
_, err := q.db.Exec(ctx, addNode,
arg.SaleBlock,
arg.SaleTxIndex,
arg.NodeID,
arg.TierIndex,
arg.DelegatedTo,
arg.OwnerPublicKey,
arg.PurchaseTxHash,
arg.DelegateTxHash,
)
return err
}
const clearDelegate = `-- name: ClearDelegate :execrows
UPDATE nodes
SET "delegated_to" = ''
WHERE "delegate_tx_hash" = NULL
`
func (q *Queries) ClearDelegate(ctx context.Context) (int64, error) {
result, err := q.db.Exec(ctx, clearDelegate)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getNodeCountByTierIndex = `-- name: GetNodeCountByTierIndex :many
SELECT tiers.tier_index as tier_index, count(nodes.tier_index)
FROM generate_series($3::int,$4::int) as tiers(tier_index)
LEFT JOIN
(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)
as nodes on tiers.tier_index = nodes.tier_index
group by tiers.tier_index
ORDER BY tiers.tier_index
`
type GetNodeCountByTierIndexParams struct {
SaleBlock int32
SaleTxIndex int32
FromTier int32
ToTier int32
}
type GetNodeCountByTierIndexRow struct {
TierIndex interface{}
Count int64
}
func (q *Queries) GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error) {
rows, err := q.db.Query(ctx, getNodeCountByTierIndex,
arg.SaleBlock,
arg.SaleTxIndex,
arg.FromTier,
arg.ToTier,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetNodeCountByTierIndexRow
for rows.Next() {
var i GetNodeCountByTierIndexRow
if err := rows.Scan(&i.TierIndex, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNodes = `-- name: GetNodes :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 AND
node_id = ANY ($3::int[])
`
type GetNodesParams struct {
SaleBlock int32
SaleTxIndex int32
NodeIds []int32
}
func (q *Queries) GetNodes(ctx context.Context, arg GetNodesParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodes, arg.SaleBlock, arg.SaleTxIndex, arg.NodeIds)
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 getNodesByOwner = `-- name: GetNodesByOwner :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 AND
owner_public_key = $3
ORDER BY tier_index
`
type GetNodesByOwnerParams struct {
SaleBlock int32
SaleTxIndex int32
OwnerPublicKey string
}
func (q *Queries) GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodesByOwner, arg.SaleBlock, arg.SaleTxIndex, arg.OwnerPublicKey)
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 getNodesByPubkey = `-- name: GetNodesByPubkey :many
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash, tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata
FROM nodes JOIN events ON nodes.purchase_tx_hash = events.tx_hash
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
owner_public_key = $3 AND
delegated_to = $4
`
type GetNodesByPubkeyParams struct {
SaleBlock int32
SaleTxIndex int32
OwnerPublicKey string
DelegatedTo string
}
type GetNodesByPubkeyRow struct {
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
DelegatedTo string
OwnerPublicKey string
PurchaseTxHash string
DelegateTxHash string
TxHash string
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
Action int32
RawMessage []byte
ParsedMessage []byte
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
}
func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]GetNodesByPubkeyRow, error) {
rows, err := q.db.Query(ctx, getNodesByPubkey,
arg.SaleBlock,
arg.SaleTxIndex,
arg.OwnerPublicKey,
arg.DelegatedTo,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetNodesByPubkeyRow
for rows.Next() {
var i GetNodesByPubkeyRow
if err := rows.Scan(
&i.SaleBlock,
&i.SaleTxIndex,
&i.NodeID,
&i.TierIndex,
&i.DelegatedTo,
&i.OwnerPublicKey,
&i.PurchaseTxHash,
&i.DelegateTxHash,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.WalletAddress,
&i.Valid,
&i.Action,
&i.RawMessage,
&i.ParsedMessage,
&i.BlockTimestamp,
&i.BlockHash,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setDelegates = `-- name: SetDelegates :execrows
UPDATE nodes
SET delegated_to = $3
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
node_id = ANY ($4::int[])
`
type SetDelegatesParams struct {
SaleBlock int32
SaleTxIndex int32
Delegatee string
NodeIds []int32
}
func (q *Queries) SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error) {
result, err := q.db.Exec(ctx, setDelegates,
arg.SaleBlock,
arg.SaleTxIndex,
arg.Delegatee,
arg.NodeIds,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -1,92 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: nodesales.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const addNodesale = `-- name: AddNodesale :exec
INSERT INTO node_sales("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type AddNodesaleParams struct {
BlockHeight int32
TxIndex int32
Name string
StartsAt pgtype.Timestamp
EndsAt pgtype.Timestamp
Tiers [][]byte
SellerPublicKey string
MaxPerAddress int32
DeployTxHash string
MaxDiscountPercentage int32
SellerWallet string
}
func (q *Queries) AddNodesale(ctx context.Context, arg AddNodesaleParams) error {
_, err := q.db.Exec(ctx, addNodesale,
arg.BlockHeight,
arg.TxIndex,
arg.Name,
arg.StartsAt,
arg.EndsAt,
arg.Tiers,
arg.SellerPublicKey,
arg.MaxPerAddress,
arg.DeployTxHash,
arg.MaxDiscountPercentage,
arg.SellerWallet,
)
return err
}
const getNodesale = `-- name: GetNodesale :many
SELECT block_height, tx_index, name, starts_at, ends_at, tiers, seller_public_key, max_per_address, deploy_tx_hash, max_discount_percentage, seller_wallet
FROM node_sales
WHERE block_height = $1 AND
tx_index = $2
`
type GetNodesaleParams struct {
BlockHeight int32
TxIndex int32
}
func (q *Queries) GetNodesale(ctx context.Context, arg GetNodesaleParams) ([]NodeSale, error) {
rows, err := q.db.Query(ctx, getNodesale, arg.BlockHeight, arg.TxIndex)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NodeSale
for rows.Next() {
var i NodeSale
if err := rows.Scan(
&i.BlockHeight,
&i.TxIndex,
&i.Name,
&i.StartsAt,
&i.EndsAt,
&i.Tiers,
&i.SellerPublicKey,
&i.MaxPerAddress,
&i.DeployTxHash,
&i.MaxDiscountPercentage,
&i.SellerWallet,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -1,31 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package gen
import (
"context"
)
type Querier interface {
AddBlock(ctx context.Context, arg AddBlockParams) error
AddEvent(ctx context.Context, arg AddEventParams) error
AddNode(ctx context.Context, arg AddNodeParams) error
AddNodesale(ctx context.Context, arg AddNodesaleParams) error
ClearDelegate(ctx context.Context) (int64, error)
ClearEvents(ctx context.Context) error
GetBlock(ctx context.Context, blockHeight int32) (Block, error)
GetEventsByWallet(ctx context.Context, walletAddress string) ([]Event, error)
GetLastProcessedBlock(ctx context.Context) (Block, error)
GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error)
GetNodes(ctx context.Context, arg GetNodesParams) ([]Node, error)
GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams) ([]Node, error)
GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]GetNodesByPubkeyRow, error)
GetNodesale(ctx context.Context, arg GetNodesaleParams) ([]NodeSale, error)
RemoveBlockFrom(ctx context.Context, fromBlock int32) (int64, error)
RemoveEventsFromBlock(ctx context.Context, fromBlock int32) (int64, error)
SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error)
}
var _ Querier = (*Queries)(nil)

View File

@@ -1,20 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: test.sql
package gen
import (
"context"
)
const clearEvents = `-- name: ClearEvents :exec
DELETE FROM events
WHERE tx_hash <> ''
`
func (q *Queries) ClearEvents(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearEvents)
return err
}

View File

@@ -1,24 +0,0 @@
package postgres
import (
db "github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5"
)
type Repository struct {
Db db.TxQueryable
Queries gen.Querier
}
func NewRepository(db db.DB) *Repository {
return &Repository{
Db: db,
Queries: gen.New(db),
}
}
func (q *Repository) WithTx(tx pgx.Tx) gen.Querier {
queries := gen.Queries{}
return queries.WithTx(tx)
}

View File

@@ -1,25 +0,0 @@
package nodesale
import "github.com/btcsuite/btcd/txscript"
func extractTapScript(witness [][]byte) (tokenizer txscript.ScriptTokenizer, controlBlock *txscript.ControlBlock, isTapScript bool) {
witness = removeAnnexFromWitness(witness)
if len(witness) < 2 {
return txscript.ScriptTokenizer{}, nil, false
}
script := witness[len(witness)-2]
rawControl := witness[len(witness)-1]
parsedControl, err := txscript.ParseControlBlock(rawControl)
if err != nil {
return txscript.ScriptTokenizer{}, nil, false
}
return txscript.MakeScriptTokenizer(0, script), parsedControl, true
}
func removeAnnexFromWitness(witness [][]byte) [][]byte {
if len(witness) >= 2 && len(witness[len(witness)-1]) > 0 && witness[len(witness)-1][0] == txscript.TaprootAnnexTag {
return witness[:len(witness)-1]
}
return witness
}

View File

@@ -31,7 +31,6 @@ type Processor struct {
bitcoinClient btcclient.Contract
network common.Network
reportingClient *reportingclient.ReportingClient
cleanupFuncs []func(context.Context) error
newRuneEntries map[runes.RuneId]*runes.RuneEntry
newRuneEntryStates map[runes.RuneId]*runes.RuneEntry
@@ -41,14 +40,13 @@ type Processor struct {
newRuneTxs []*entity.RuneTransaction
}
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, network common.Network, reportingClient *reportingclient.ReportingClient, cleanupFuncs []func(context.Context) error) *Processor {
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, network common.Network, reportingClient *reportingclient.ReportingClient) *Processor {
return &Processor{
runesDg: runesDg,
indexerInfoDg: indexerInfoDg,
bitcoinClient: bitcoinClient,
network: network,
reportingClient: reportingClient,
cleanupFuncs: cleanupFuncs,
newRuneEntries: make(map[runes.RuneId]*runes.RuneEntry),
newRuneEntryStates: make(map[runes.RuneId]*runes.RuneEntry),
newOutPointBalances: make(map[wire.OutPoint][]*entity.OutPointBalance),
@@ -230,13 +228,3 @@ func (p *Processor) RevertData(ctx context.Context, from int64) error {
}
return nil
}
func (p *Processor) Shutdown(ctx context.Context) error {
var errs []error
for _, cleanup := range p.cleanupFuncs {
if err := cleanup(ctx); err != nil {
errs = append(errs, err)
}
}
return errors.WithStack(errors.Join(errs...))
}

View File

@@ -33,7 +33,6 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
runesDg runesdatagateway.RunesDataGateway
indexerInfoDg runesdatagateway.IndexerInfoDataGateway
)
var cleanupFuncs []func(context.Context) error
switch strings.ToLower(conf.Modules.Runes.Database) {
case "postgresql", "postgres", "pg":
pg, err := postgres.NewPool(ctx, conf.Modules.Runes.Postgres)
@@ -43,10 +42,7 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
}
return nil, errors.Wrap(err, "can't create Postgres connection pool")
}
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
pg.Close()
return nil
})
defer pg.Close()
runesRepo := runespostgres.NewRepository(pg)
runesDg = runesRepo
indexerInfoDg = runesRepo
@@ -66,7 +62,7 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
return nil, errors.Wrapf(errs.Unsupported, "%q datasource is not supported", conf.Modules.Runes.Datasource)
}
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient, cleanupFuncs)
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient)
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
}

View File

@@ -1,7 +1,6 @@
package runes
import (
"fmt"
"log"
"slices"
"unicode/utf8"
@@ -335,7 +334,6 @@ func runestonePayloadFromTx(tx *types.Transaction) ([]byte, Flaws) {
continue
}
if err := tokenizer.Err(); err != nil {
fmt.Println(err.Error())
continue
}
if opCode := tokenizer.Opcode(); opCode != RUNESTONE_PAYLOAD_MAGIC_NUMBER {

View File

@@ -9,4 +9,5 @@ import (
type Contract interface {
GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error)
GetTransactionOutputs(ctx context.Context, txHash chainhash.Hash) ([]*types.TxOut, error)
}

Some files were not shown because too many files have changed in this diff Show More