mirror of
https://github.com/alexgo-io/gaze-brc20-indexer.git
synced 2026-01-12 22:22:19 +08:00
Merge branch 'feature/brc20-module-api' into feat/brc20-module
This commit is contained in:
@@ -51,8 +51,6 @@ 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
|
||||
@@ -108,7 +106,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:v1.0.0
|
||||
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
|
||||
container_name: gaze-indexer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -23,10 +23,15 @@ import (
|
||||
"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"
|
||||
@@ -137,6 +142,14 @@ 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{}) {
|
||||
|
||||
6
common/http.go
Normal file
6
common/http.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package common
|
||||
|
||||
type HttpResponse[T any] struct {
|
||||
Error *string `json:"error"`
|
||||
Result *T `json:"result,omitempty"`
|
||||
}
|
||||
@@ -23,6 +23,14 @@ 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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
Version = "v0.0.1"
|
||||
Version = "v0.2.1"
|
||||
)
|
||||
|
||||
@@ -91,6 +91,10 @@ 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
|
||||
@@ -204,9 +208,9 @@ func (i *Indexer[T]) process(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
// validate is input is continuous and no reorg
|
||||
for i := 1; i < len(inputs); i++ {
|
||||
header := inputs[i].BlockHeader()
|
||||
prevHeader := inputs[i-1].BlockHeader()
|
||||
prevHeader := i.currentBlock
|
||||
for i, input := range inputs {
|
||||
header := input.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)
|
||||
}
|
||||
@@ -217,6 +221,7 @@ 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)))
|
||||
|
||||
@@ -29,6 +29,9 @@ 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 {
|
||||
|
||||
21
go.mod
21
go.mod
@@ -6,6 +6,7 @@ require (
|
||||
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11
|
||||
github.com/btcsuite/btcd v0.24.0
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
github.com/cockroachdb/errors v1.11.1
|
||||
github.com/gaze-network/uint128 v1.3.0
|
||||
@@ -21,23 +22,24 @@ require (
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/sync v0.5.0
|
||||
golang.org/x/sync v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.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
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
|
||||
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/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // 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
|
||||
@@ -45,6 +47,7 @@ require (
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/holiman/uint256 v1.2.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
@@ -75,10 +78,10 @@ require (
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
50
go.sum
50
go.sum
@@ -7,18 +7,23 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1 h1:1Qku9k9WgzobjqBY7tT3CLjWxtTJZxkYNhOV6QeCTjY=
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1/go.mod h1:PR60twfJIaHEo9Wb6eJBh8nBHEZIQQx8CvRwh0YmEPk=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo=
|
||||
github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 h1:UmfOIiWMZcVMOLaN+lxbbLSuoINGS1WmK1TZNI0b4yk=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
@@ -50,10 +55,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg=
|
||||
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA=
|
||||
@@ -97,8 +104,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
||||
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=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
@@ -112,6 +119,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
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/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
|
||||
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
@@ -221,8 +230,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
@@ -247,14 +257,14 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -269,8 +279,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -283,19 +293,19 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
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"
|
||||
@@ -66,6 +68,8 @@ type Modules struct {
|
||||
|
||||
type HTTPServerConfig struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Logger requestlogger.Config `mapstructure:"logger"`
|
||||
RequestIP requestcontext.WithClientIPConfig `mapstructure:"requestip"`
|
||||
}
|
||||
|
||||
// Parse parse the configuration from environment variables
|
||||
|
||||
115
modules/brc20/api/httphandler/get_balances_by_address.go
Normal file
115
modules/brc20/api/httphandler/get_balances_by_address.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"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/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getBalancesByAddressRequest struct {
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getBalancesByAddressRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type balanceExtend struct {
|
||||
Transferable *uint256.Int `json:"transferable"`
|
||||
Available *uint256.Int `json:"available"`
|
||||
}
|
||||
|
||||
type balance struct {
|
||||
Amount *uint256.Int `json:"amount"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
Extend balanceExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getBalancesByAddressResult struct {
|
||||
List []balance `json:"list"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
}
|
||||
|
||||
type getBalancesByAddressResponse = common.HttpResponse[getBalancesByAddressResult]
|
||||
|
||||
func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
|
||||
var req getBalancesByAddressRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pkScript, err := btcutils.ToPkScript(h.network, req.Wallet)
|
||||
if err != nil {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetBalancesByPkScript")
|
||||
}
|
||||
|
||||
ticks := lo.Keys(balances)
|
||||
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), ticks)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
|
||||
}
|
||||
|
||||
balanceList := make([]balance, 0, len(balances))
|
||||
for id, b := range balances {
|
||||
entry := entries[id]
|
||||
balanceList = append(balanceList, balance{
|
||||
Amount: decimals.ToUint256(b.OverallBalance, entry.Decimals),
|
||||
Id: id,
|
||||
Name: entry.OriginalTick,
|
||||
Symbol: entry.Tick,
|
||||
Decimals: entry.Decimals,
|
||||
Extend: balanceExtend{
|
||||
Transferable: decimals.ToUint256(b.OverallBalance.Sub(b.AvailableBalance), entry.Decimals),
|
||||
Available: decimals.ToUint256(b.AvailableBalance, entry.Decimals),
|
||||
},
|
||||
})
|
||||
}
|
||||
slices.SortFunc(balanceList, func(i, j balance) int {
|
||||
return j.Amount.Cmp(i.Amount)
|
||||
})
|
||||
|
||||
resp := getBalancesByAddressResponse{
|
||||
Result: &getBalancesByAddressResult{
|
||||
BlockHeight: blockHeight,
|
||||
List: balanceList,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
125
modules/brc20/api/httphandler/get_balances_by_address_batch.go
Normal file
125
modules/brc20/api/httphandler/get_balances_by_address_batch.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"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/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type getBalancesByAddressBatchRequest struct {
|
||||
Queries []getBalancesByAddressRequest `json:"queries"`
|
||||
}
|
||||
|
||||
func (r getBalancesByAddressBatchRequest) Validate() error {
|
||||
var errList []error
|
||||
for _, query := range r.Queries {
|
||||
if query.Wallet == "" {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required"))
|
||||
}
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getBalancesByAddressBatchResult struct {
|
||||
List []*getBalancesByAddressResult `json:"list"`
|
||||
}
|
||||
|
||||
type getBalancesByAddressBatchResponse = common.HttpResponse[getBalancesByAddressBatchResult]
|
||||
|
||||
func (h *HttpHandler) GetBalancesByAddressBatch(ctx *fiber.Ctx) (err error) {
|
||||
var req getBalancesByAddressBatchRequest
|
||||
if err := ctx.BodyParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var latestBlockHeight uint64
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
latestBlockHeight = uint64(blockHeader.Height)
|
||||
|
||||
processQuery := func(ctx context.Context, query getBalancesByAddressRequest) (*getBalancesByAddressResult, error) {
|
||||
pkScript, err := btcutils.ToPkScript(h.network, query.Wallet)
|
||||
if err != nil {
|
||||
return nil, errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
blockHeight := query.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeight = latestBlockHeight
|
||||
}
|
||||
|
||||
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
|
||||
}
|
||||
|
||||
balanceRuneIds := lo.Keys(balances)
|
||||
entries, err := h.usecase.GetTickEntryByTickBatch(ctx, balanceRuneIds)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetTickEntryByTickBatch")
|
||||
}
|
||||
|
||||
balanceList := make([]balance, 0, len(balances))
|
||||
for id, b := range balances {
|
||||
entry := entries[id]
|
||||
balanceList = append(balanceList, balance{
|
||||
Amount: decimals.ToUint256(b.OverallBalance, entry.Decimals),
|
||||
Id: id,
|
||||
Name: entry.OriginalTick,
|
||||
Symbol: entry.Tick,
|
||||
Decimals: entry.Decimals,
|
||||
Extend: balanceExtend{
|
||||
Transferable: decimals.ToUint256(b.OverallBalance.Sub(b.AvailableBalance), entry.Decimals),
|
||||
Available: decimals.ToUint256(b.AvailableBalance, entry.Decimals),
|
||||
},
|
||||
})
|
||||
}
|
||||
slices.SortFunc(balanceList, func(i, j balance) int {
|
||||
return j.Amount.Cmp(i.Amount)
|
||||
})
|
||||
|
||||
return &getBalancesByAddressResult{
|
||||
BlockHeight: blockHeight,
|
||||
List: balanceList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
results := make([]*getBalancesByAddressResult, len(req.Queries))
|
||||
eg, ectx := errgroup.WithContext(ctx.UserContext())
|
||||
for i, query := range req.Queries {
|
||||
i := i
|
||||
query := query
|
||||
eg.Go(func() error {
|
||||
result, err := processQuery(ectx, query)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error during processQuery for query %d", i)
|
||||
}
|
||||
results[i] = result
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp := getBalancesByAddressBatchResponse{
|
||||
Result: &getBalancesByAddressBatchResult{
|
||||
List: results,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
49
modules/brc20/api/httphandler/get_current_block.go
Normal file
49
modules/brc20/api/httphandler/get_current_block.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// TODO: use modules/brc20/constants.go
|
||||
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")),
|
||||
},
|
||||
}
|
||||
|
||||
type getCurrentBlockResult struct {
|
||||
Hash string `json:"hash"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
type getCurrentBlockResponse = common.HttpResponse[getCurrentBlockResult]
|
||||
|
||||
func (h *HttpHandler) GetCurrentBlock(ctx *fiber.Ctx) (err error) {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if !errors.Is(err, errs.NotFound) {
|
||||
return errors.Wrap(err, "error during get latest block")
|
||||
}
|
||||
blockHeader = startingBlockHeader[h.network]
|
||||
}
|
||||
|
||||
resp := getCurrentBlockResponse{
|
||||
Result: &getCurrentBlockResult{
|
||||
Hash: blockHeader.Hash.String(),
|
||||
Height: blockHeader.Height,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
107
modules/brc20/api/httphandler/get_holders.go
Normal file
107
modules/brc20/api/httphandler/get_holders.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"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/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
type getHoldersRequest struct {
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getHoldersRequest) Validate() error {
|
||||
var errList []error
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type holdingBalanceExtend struct {
|
||||
Transferable *uint256.Int `json:"transferable"`
|
||||
Available *uint256.Int `json:"available"`
|
||||
}
|
||||
|
||||
type holdingBalance struct {
|
||||
Address string `json:"address"`
|
||||
PkScript string `json:"pkScript"`
|
||||
Amount *uint256.Int `json:"amount"`
|
||||
Percent float64 `json:"percent"`
|
||||
Extend holdingBalanceExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getHoldersResult struct {
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
TotalSupply *uint256.Int `json:"totalSupply"`
|
||||
MintedAmount *uint256.Int `json:"mintedAmount"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
List []holdingBalance `json:"list"`
|
||||
}
|
||||
|
||||
type getHoldersResponse = common.HttpResponse[getHoldersResult]
|
||||
|
||||
func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
var req getHoldersRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
entry, err := h.usecase.GetTickEntryByTickAndHeight(ctx.UserContext(), req.Id, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTickEntryByTickAndHeight")
|
||||
}
|
||||
holdingBalances, err := h.usecase.GetBalancesByTick(ctx.UserContext(), req.Id, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetBalancesByTick")
|
||||
}
|
||||
|
||||
list := make([]holdingBalance, 0, len(holdingBalances))
|
||||
for _, balance := range holdingBalances {
|
||||
address, err := btcutils.PkScriptToAddress(balance.PkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "can't convert pkscript(%x) to address", balance.PkScript)
|
||||
}
|
||||
percent := balance.OverallBalance.Div(entry.TotalSupply)
|
||||
list = append(list, holdingBalance{
|
||||
Address: address,
|
||||
PkScript: hex.EncodeToString(balance.PkScript),
|
||||
Amount: decimals.ToUint256(balance.OverallBalance, entry.Decimals),
|
||||
Percent: percent.InexactFloat64(),
|
||||
Extend: holdingBalanceExtend{
|
||||
Transferable: decimals.ToUint256(balance.OverallBalance.Sub(balance.AvailableBalance), entry.Decimals),
|
||||
Available: decimals.ToUint256(balance.AvailableBalance, entry.Decimals),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
resp := getHoldersResponse{
|
||||
Result: &getHoldersResult{
|
||||
BlockHeight: blockHeight,
|
||||
TotalSupply: decimals.ToUint256(entry.TotalSupply, entry.Decimals), // TODO: convert to wei
|
||||
MintedAmount: decimals.ToUint256(entry.MintedAmount, entry.Decimals), // TODO: convert to wei
|
||||
List: list,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
150
modules/brc20/api/httphandler/get_token_info.go
Normal file
150
modules/brc20/api/httphandler/get_token_info.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type getTokenInfoRequest struct {
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getTokenInfoRequest) Validate() error {
|
||||
var errList []error
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type tokenInfoExtend struct {
|
||||
DeployedBy string `json:"deployedBy"`
|
||||
LimitPerMint *uint256.Int `json:"limitPerMint"`
|
||||
DeployInscriptionId string `json:"deployInscriptionId"`
|
||||
DeployInscriptionNumber int64 `json:"deployInscriptionNumber"`
|
||||
InscriptionStartNumber int64 `json:"inscriptionStartNumber"`
|
||||
InscriptionEndNumber int64 `json:"inscriptionEndNumber"`
|
||||
}
|
||||
|
||||
type getTokenInfoResult struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
TotalSupply *uint256.Int `json:"totalSupply"`
|
||||
CirculatingSupply *uint256.Int `json:"circulatingSupply"`
|
||||
MintedAmount *uint256.Int `json:"mintedAmount"`
|
||||
BurnedAmount *uint256.Int `json:"burnedAmount"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
DeployedAt uint64 `json:"deployedAt"`
|
||||
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
||||
CompletedAt *uint64 `json:"completedAt"`
|
||||
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
||||
HoldersCount int `json:"holdersCount"`
|
||||
Extend tokenInfoExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getTokenInfoResponse = common.HttpResponse[getTokenInfoResult]
|
||||
|
||||
func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokenInfoRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
group, groupctx := errgroup.WithContext(ctx.UserContext())
|
||||
var (
|
||||
entry *entity.TickEntry
|
||||
firstInscriptionNumber, lastInscriptionNumber int64
|
||||
deployEvent *entity.EventDeploy
|
||||
holdingBalances []*entity.Balance
|
||||
)
|
||||
group.Go(func() error {
|
||||
deployEvent, err = h.usecase.GetDeployEventByTick(groupctx, req.Id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetDeployEventByTick")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
group.Go(func() error {
|
||||
// TODO: at block height to parameter.
|
||||
firstInscriptionNumber, lastInscriptionNumber, err = h.usecase.GetFirstLastInscriptionNumberByTick(groupctx, req.Id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetFirstLastInscriptionNumberByTick")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
group.Go(func() error {
|
||||
entry, err = h.usecase.GetTickEntryByTickAndHeight(groupctx, req.Id, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTickEntryByTickAndHeight")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
group.Go(func() error {
|
||||
balances, err := h.usecase.GetBalancesByTick(groupctx, req.Id, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
holdingBalances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
|
||||
return !b.OverallBalance.IsZero()
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err := group.Wait(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
address, err := btcutils.PkScriptToAddress(deployEvent.PkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for pkscript: %x, network: %v`, deployEvent.PkScript, h.network)
|
||||
}
|
||||
|
||||
resp := getTokenInfoResponse{
|
||||
Result: &getTokenInfoResult{
|
||||
Id: entry.Tick,
|
||||
Name: entry.OriginalTick,
|
||||
Symbol: entry.Tick,
|
||||
TotalSupply: decimals.ToUint256(entry.TotalSupply, entry.Decimals),
|
||||
CirculatingSupply: decimals.ToUint256(entry.MintedAmount.Sub(entry.BurnedAmount), entry.Decimals),
|
||||
MintedAmount: decimals.ToUint256(entry.MintedAmount, entry.Decimals),
|
||||
BurnedAmount: decimals.ToUint256(entry.BurnedAmount, entry.Decimals),
|
||||
Decimals: entry.Decimals,
|
||||
DeployedAt: uint64(entry.DeployedAt.Unix()),
|
||||
DeployedAtHeight: entry.DeployedAtHeight,
|
||||
CompletedAt: lo.Ternary(entry.CompletedAt.IsZero(), nil, lo.ToPtr(uint64(entry.CompletedAt.Unix()))),
|
||||
CompletedAtHeight: lo.Ternary(entry.CompletedAtHeight == 0, nil, lo.ToPtr(entry.CompletedAtHeight)),
|
||||
HoldersCount: len(holdingBalances),
|
||||
Extend: tokenInfoExtend{
|
||||
DeployedBy: address,
|
||||
LimitPerMint: decimals.ToUint256(entry.LimitPerMint, entry.Decimals),
|
||||
DeployInscriptionId: deployEvent.InscriptionId.String(),
|
||||
DeployInscriptionNumber: deployEvent.InscriptionNumber,
|
||||
InscriptionStartNumber: lo.Ternary(firstInscriptionNumber < 0, deployEvent.InscriptionNumber, firstInscriptionNumber),
|
||||
InscriptionEndNumber: lo.Ternary(lastInscriptionNumber < 0, deployEvent.InscriptionNumber, lastInscriptionNumber),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
454
modules/brc20/api/httphandler/get_transactions.go
Normal file
454
modules/brc20/api/httphandler/get_transactions.go
Normal file
@@ -0,0 +1,454 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/samber/lo"
|
||||
"github.com/shopspring/decimal"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var ops = []string{"inscribe-deploy", "inscribe-mint", "inscribe-transfer", "transfer-transfer"}
|
||||
|
||||
type getTransactionsRequest struct {
|
||||
Wallet string `query:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Op string `query:"op"`
|
||||
}
|
||||
|
||||
func (r getTransactionsRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Op != "" {
|
||||
if !lo.Contains(ops, r.Op) {
|
||||
errList = append(errList, errors.Errorf("invalid 'op' value: %s, supported values: %s", r.Op, strings.Join(ops, ", ")))
|
||||
}
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type txOpDeployArg struct {
|
||||
Op string `json:"op"`
|
||||
Tick string `json:"tick"`
|
||||
Max decimal.Decimal `json:"max"`
|
||||
Lim decimal.Decimal `json:"lim"`
|
||||
Dec uint16 `json:"dec"`
|
||||
SelfMint bool `json:"self_mint"`
|
||||
}
|
||||
|
||||
type txOpGeneralArg struct {
|
||||
Op string `json:"op"`
|
||||
Tick string `json:"tick"`
|
||||
Amount decimal.Decimal `json:"amt"`
|
||||
}
|
||||
|
||||
type txOperation[T any] struct {
|
||||
InscriptionId string `json:"inscriptionId"`
|
||||
InscriptionNumber int64 `json:"inscriptionNumber"`
|
||||
Op string `json:"op"`
|
||||
Args T `json:"args"`
|
||||
}
|
||||
|
||||
type txOperationsDeploy struct {
|
||||
txOperation[txOpDeployArg]
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type txOperationsMint struct {
|
||||
txOperation[txOpGeneralArg]
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type txOperationsInscribeTransfer struct {
|
||||
txOperation[txOpGeneralArg]
|
||||
Address string `json:"address"`
|
||||
OutputIndex uint32 `json:"outputIndex"`
|
||||
Sats uint64 `json:"sats"`
|
||||
}
|
||||
|
||||
type txOperationsTransferTransfer struct {
|
||||
txOperation[txOpGeneralArg]
|
||||
FromAddress string `json:"fromAddress"`
|
||||
ToAddress string `json:"toAddress"`
|
||||
}
|
||||
|
||||
type transactionExtend struct {
|
||||
Operations []any `json:"operations"`
|
||||
}
|
||||
|
||||
type amountWithDecimal struct {
|
||||
Amount *uint256.Int `json:"amount"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
}
|
||||
|
||||
type txInputOutput struct {
|
||||
PkScript string `json:"pkScript"`
|
||||
Address string `json:"address"`
|
||||
Id string `json:"id"`
|
||||
Amount *uint256.Int `json:"amount"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
Index uint32 `json:"index"`
|
||||
}
|
||||
|
||||
type transaction struct {
|
||||
TxHash chainhash.Hash `json:"txHash"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
Index uint32 `json:"index"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Inputs []txInputOutput `json:"inputs"`
|
||||
Outputs []txInputOutput `json:"outputs"`
|
||||
Mints map[string]amountWithDecimal `json:"mints"`
|
||||
Burns map[string]amountWithDecimal `json:"burns"`
|
||||
Extend transactionExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getTransactionsResult struct {
|
||||
List []transaction `json:"list"`
|
||||
}
|
||||
|
||||
type getTransactionsResponse = common.HttpResponse[getTransactionsResult]
|
||||
|
||||
func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
var req getTransactionsRequest
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var pkScript []byte
|
||||
if req.Wallet != "" {
|
||||
pkScript, err = btcutils.ToPkScript(h.network, req.Wallet)
|
||||
if err != nil {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
// set blockHeight to the latest block height blockHeight, pkScript, and runeId are not provided
|
||||
if blockHeight == 0 && pkScript == nil && req.Id == "" {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
var (
|
||||
deployEvents []*entity.EventDeploy
|
||||
mintEvents []*entity.EventMint
|
||||
transferTransferEvents []*entity.EventTransferTransfer
|
||||
inscribeTransferEvents []*entity.EventInscribeTransfer
|
||||
)
|
||||
|
||||
group, groupctx := errgroup.WithContext(ctx.UserContext())
|
||||
|
||||
if req.Op == "" || req.Op == "inscribe-deploy" {
|
||||
group.Go(func() error {
|
||||
events, err := h.usecase.GetDeployEvents(groupctx, pkScript, req.Id, blockHeight)
|
||||
deployEvents = events
|
||||
return errors.Wrap(err, "error during get inscribe-deploy events")
|
||||
})
|
||||
}
|
||||
if req.Op == "" || req.Op == "inscribe-mint" {
|
||||
group.Go(func() error {
|
||||
events, err := h.usecase.GetMintEvents(groupctx, pkScript, req.Id, blockHeight)
|
||||
mintEvents = events
|
||||
return errors.Wrap(err, "error during get inscribe-mint events")
|
||||
})
|
||||
}
|
||||
if req.Op == "" || req.Op == "transfer-transfer" {
|
||||
group.Go(func() error {
|
||||
events, err := h.usecase.GetTransferTransferEvents(groupctx, pkScript, req.Id, blockHeight)
|
||||
transferTransferEvents = events
|
||||
return errors.Wrap(err, "error during get transfer-transfer events")
|
||||
})
|
||||
}
|
||||
if req.Op == "" || req.Op == "inscribe-transfer" {
|
||||
group.Go(func() error {
|
||||
events, err := h.usecase.GetInscribeTransferEvents(groupctx, pkScript, req.Id, blockHeight)
|
||||
inscribeTransferEvents = events
|
||||
return errors.Wrap(err, "error during get inscribe-transfer events")
|
||||
})
|
||||
}
|
||||
if err := group.Wait(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
allTicks := make([]string, 0, len(deployEvents)+len(mintEvents)+len(transferTransferEvents)+len(inscribeTransferEvents))
|
||||
allTicks = append(allTicks, lo.Map(deployEvents, func(event *entity.EventDeploy, _ int) string { return event.Tick })...)
|
||||
allTicks = append(allTicks, lo.Map(mintEvents, func(event *entity.EventMint, _ int) string { return event.Tick })...)
|
||||
allTicks = append(allTicks, lo.Map(transferTransferEvents, func(event *entity.EventTransferTransfer, _ int) string { return event.Tick })...)
|
||||
allTicks = append(allTicks, lo.Map(inscribeTransferEvents, func(event *entity.EventInscribeTransfer, _ int) string { return event.Tick })...)
|
||||
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), lo.Uniq(allTicks))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
|
||||
}
|
||||
|
||||
rawTxList := make([]transaction, 0, len(deployEvents)+len(mintEvents)+len(transferTransferEvents)+len(inscribeTransferEvents))
|
||||
|
||||
// Deploy events
|
||||
for _, event := range deployEvents {
|
||||
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
|
||||
}
|
||||
respTx := transaction{
|
||||
TxHash: event.TxHash,
|
||||
BlockHeight: event.BlockHeight,
|
||||
Index: event.TxIndex,
|
||||
Timestamp: event.Timestamp.Unix(),
|
||||
Mints: map[string]amountWithDecimal{},
|
||||
Burns: map[string]amountWithDecimal{},
|
||||
Extend: transactionExtend{
|
||||
Operations: []any{
|
||||
txOperationsDeploy{
|
||||
txOperation: txOperation[txOpDeployArg]{
|
||||
InscriptionId: event.InscriptionId.String(),
|
||||
InscriptionNumber: event.InscriptionNumber,
|
||||
Op: "deploy",
|
||||
Args: txOpDeployArg{
|
||||
Op: "deploy",
|
||||
Tick: event.Tick,
|
||||
Max: event.TotalSupply,
|
||||
Lim: event.LimitPerMint,
|
||||
Dec: event.Decimals,
|
||||
SelfMint: event.IsSelfMint,
|
||||
},
|
||||
},
|
||||
Address: address,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
rawTxList = append(rawTxList, respTx)
|
||||
}
|
||||
|
||||
// Mint events
|
||||
for _, event := range mintEvents {
|
||||
entry := entries[event.Tick]
|
||||
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
|
||||
}
|
||||
amtWei := decimals.ToUint256(event.Amount, entry.Decimals)
|
||||
respTx := transaction{
|
||||
TxHash: event.TxHash,
|
||||
BlockHeight: event.BlockHeight,
|
||||
Index: event.TxIndex,
|
||||
Timestamp: event.Timestamp.Unix(),
|
||||
Outputs: []txInputOutput{
|
||||
{
|
||||
PkScript: hex.EncodeToString(event.PkScript),
|
||||
Address: address,
|
||||
Id: event.Tick,
|
||||
Amount: amtWei,
|
||||
Decimals: entry.Decimals,
|
||||
Index: event.TxIndex,
|
||||
},
|
||||
},
|
||||
Mints: map[string]amountWithDecimal{
|
||||
event.Tick: {
|
||||
Amount: amtWei,
|
||||
Decimals: entry.Decimals,
|
||||
},
|
||||
},
|
||||
Extend: transactionExtend{
|
||||
Operations: []any{
|
||||
txOperationsMint{
|
||||
txOperation: txOperation[txOpGeneralArg]{
|
||||
InscriptionId: event.InscriptionId.String(),
|
||||
InscriptionNumber: event.InscriptionNumber,
|
||||
Op: "inscribe-mint",
|
||||
Args: txOpGeneralArg{
|
||||
Op: "inscribe-mint",
|
||||
Tick: event.Tick,
|
||||
Amount: event.Amount,
|
||||
},
|
||||
},
|
||||
Address: address,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
rawTxList = append(rawTxList, respTx)
|
||||
}
|
||||
|
||||
// Inscribe Transfer events
|
||||
for _, event := range inscribeTransferEvents {
|
||||
address, err := btcutils.PkScriptToAddress(event.PkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.PkScript, h.network)
|
||||
}
|
||||
respTx := transaction{
|
||||
TxHash: event.TxHash,
|
||||
BlockHeight: event.BlockHeight,
|
||||
Index: event.TxIndex,
|
||||
Timestamp: event.Timestamp.Unix(),
|
||||
Mints: map[string]amountWithDecimal{},
|
||||
Burns: map[string]amountWithDecimal{},
|
||||
Extend: transactionExtend{
|
||||
Operations: []any{
|
||||
txOperationsInscribeTransfer{
|
||||
txOperation: txOperation[txOpGeneralArg]{
|
||||
InscriptionId: event.InscriptionId.String(),
|
||||
InscriptionNumber: event.InscriptionNumber,
|
||||
Op: "inscribe-transfer",
|
||||
Args: txOpGeneralArg{
|
||||
Op: "inscribe-transfer",
|
||||
Tick: event.Tick,
|
||||
Amount: event.Amount,
|
||||
},
|
||||
},
|
||||
Address: address,
|
||||
OutputIndex: event.SatPoint.OutPoint.Index,
|
||||
Sats: event.SatsAmount,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
rawTxList = append(rawTxList, respTx)
|
||||
}
|
||||
|
||||
// Transfer Transfer events
|
||||
for _, event := range transferTransferEvents {
|
||||
entry := entries[event.Tick]
|
||||
amntWei := decimals.ToUint256(event.Amount, entry.Decimals)
|
||||
fromAddress, err := btcutils.PkScriptToAddress(event.FromPkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.FromPkScript, h.network)
|
||||
}
|
||||
toAddress := ""
|
||||
if len(event.ToPkScript) > 0 && !bytes.Equal(event.ToPkScript, []byte{0x6a}) {
|
||||
toAddress, err = btcutils.PkScriptToAddress(event.ToPkScript, h.network)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, `error during PkScriptToAddress for deploy event %s, pkscript: %x, network: %v`, event.TxHash, event.FromPkScript, h.network)
|
||||
}
|
||||
}
|
||||
|
||||
// if toAddress is empty, it's a burn.
|
||||
burns := map[string]amountWithDecimal{}
|
||||
if len(toAddress) == 0 {
|
||||
burns[event.Tick] = amountWithDecimal{
|
||||
Amount: amntWei,
|
||||
Decimals: entry.Decimals,
|
||||
}
|
||||
}
|
||||
|
||||
respTx := transaction{
|
||||
TxHash: event.TxHash,
|
||||
BlockHeight: event.BlockHeight,
|
||||
Index: event.TxIndex,
|
||||
Timestamp: event.Timestamp.Unix(),
|
||||
Inputs: []txInputOutput{
|
||||
{
|
||||
PkScript: hex.EncodeToString(event.FromPkScript),
|
||||
Address: fromAddress,
|
||||
Id: event.Tick,
|
||||
Amount: amntWei,
|
||||
Decimals: entry.Decimals,
|
||||
Index: event.ToOutputIndex,
|
||||
},
|
||||
},
|
||||
Outputs: []txInputOutput{
|
||||
{
|
||||
PkScript: hex.EncodeToString(event.ToPkScript),
|
||||
Address: fromAddress,
|
||||
Id: event.Tick,
|
||||
Amount: amntWei,
|
||||
Decimals: entry.Decimals,
|
||||
Index: event.ToOutputIndex,
|
||||
},
|
||||
},
|
||||
Mints: map[string]amountWithDecimal{},
|
||||
Burns: burns,
|
||||
Extend: transactionExtend{
|
||||
Operations: []any{
|
||||
txOperationsTransferTransfer{
|
||||
txOperation: txOperation[txOpGeneralArg]{
|
||||
InscriptionId: event.InscriptionId.String(),
|
||||
InscriptionNumber: event.InscriptionNumber,
|
||||
Op: "transfer-transfer",
|
||||
Args: txOpGeneralArg{
|
||||
Op: "transfer-transfer",
|
||||
Tick: event.Tick,
|
||||
Amount: event.Amount,
|
||||
},
|
||||
},
|
||||
FromAddress: fromAddress,
|
||||
ToAddress: toAddress,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
rawTxList = append(rawTxList, respTx)
|
||||
}
|
||||
|
||||
// merge brc-20 tx events that have the same tx hash
|
||||
txList := make([]transaction, 0, len(rawTxList))
|
||||
groupedTxs := lo.GroupBy(rawTxList, func(tx transaction) chainhash.Hash { return tx.TxHash })
|
||||
for _, txs := range groupedTxs {
|
||||
tx := txs[0]
|
||||
if tx.Mints == nil {
|
||||
tx.Mints = map[string]amountWithDecimal{}
|
||||
}
|
||||
if tx.Burns == nil {
|
||||
tx.Burns = map[string]amountWithDecimal{}
|
||||
}
|
||||
for _, tx2 := range txs[1:] {
|
||||
tx.Inputs = append(tx.Inputs, tx2.Inputs...)
|
||||
tx.Outputs = append(tx.Outputs, tx2.Outputs...)
|
||||
if len(tx2.Mints) > 0 {
|
||||
return errors.Wrap(errs.InvalidState, "transaction can't have multiple mints")
|
||||
}
|
||||
for tick, tx2Ammt := range tx2.Burns {
|
||||
if txAmmt, ok := tx.Burns[tick]; ok {
|
||||
tx.Burns[tick] = amountWithDecimal{
|
||||
Amount: new(uint256.Int).Add(txAmmt.Amount, tx2Ammt.Amount),
|
||||
Decimals: txAmmt.Decimals,
|
||||
}
|
||||
} else {
|
||||
tx.Burns[tick] = tx2Ammt
|
||||
}
|
||||
}
|
||||
tx.Extend.Operations = append(tx.Extend.Operations, tx2.Extend.Operations...)
|
||||
}
|
||||
slices.SortFunc(tx.Inputs, func(i, j txInputOutput) int {
|
||||
return cmp.Compare(i.Index, j.Index)
|
||||
})
|
||||
slices.SortFunc(tx.Outputs, func(i, j txInputOutput) int {
|
||||
return cmp.Compare(i.Index, j.Index)
|
||||
})
|
||||
txList = append(txList, tx)
|
||||
}
|
||||
|
||||
// sort by block height ASC, then index ASC
|
||||
slices.SortFunc(txList, func(t1, t2 transaction) int {
|
||||
if t1.BlockHeight != t2.BlockHeight {
|
||||
return int(t1.BlockHeight - t2.BlockHeight)
|
||||
}
|
||||
return int(t1.Index - t2.Index)
|
||||
})
|
||||
|
||||
resp := getTransactionsResponse{
|
||||
Result: &getTransactionsResult{
|
||||
List: txList,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
136
modules/brc20/api/httphandler/get_utxos_by_address.go
Normal file
136
modules/brc20/api/httphandler/get_utxos_by_address.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/gaze-network/indexer-network/pkg/decimals"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getUTXOsByAddressRequest struct {
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getUTXOsByAddressRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type transferableInscription struct {
|
||||
Ticker string `json:"ticker"`
|
||||
Amount *uint256.Int `json:"amount"`
|
||||
Decimals uint16 `json:"decimals"`
|
||||
}
|
||||
|
||||
type utxoExtend struct {
|
||||
TransferableInscriptions []transferableInscription `json:"transferableInscriptions"`
|
||||
}
|
||||
|
||||
type utxo struct {
|
||||
TxHash chainhash.Hash `json:"txHash"`
|
||||
OutputIndex uint32 `json:"outputIndex"`
|
||||
Extend utxoExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getUTXOsByAddressResult struct {
|
||||
List []utxo `json:"list"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
}
|
||||
|
||||
type getUTXOsByAddressResponse = common.HttpResponse[getUTXOsByAddressResult]
|
||||
|
||||
func (h *HttpHandler) GetUTXOsByAddress(ctx *fiber.Ctx) (err error) {
|
||||
var req getUTXOsByAddressRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pkScript, err := btcutils.ToPkScript(h.network, req.Wallet)
|
||||
if err != nil {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
transferables, err := h.usecase.GetTransferableTransfersByPkScript(ctx.UserContext(), pkScript, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTransferableTransfersByPkScript")
|
||||
}
|
||||
|
||||
transferableTicks := lo.Map(transferables, func(src *entity.EventInscribeTransfer, _ int) string { return src.Tick })
|
||||
entries, err := h.usecase.GetTickEntryByTickBatch(ctx.UserContext(), transferableTicks)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTickEntryByTickBatch")
|
||||
}
|
||||
|
||||
groupedtransferableTi := lo.GroupBy(transferables, func(src *entity.EventInscribeTransfer) wire.OutPoint { return src.SatPoint.OutPoint })
|
||||
utxoList := make([]utxo, 0, len(groupedtransferableTi))
|
||||
for outPoint, transferables := range groupedtransferableTi {
|
||||
transferableInscriptions := make([]transferableInscription, 0, len(transferables))
|
||||
for _, transferable := range transferables {
|
||||
entry := entries[transferable.Tick]
|
||||
transferableInscriptions = append(transferableInscriptions, transferableInscription{
|
||||
Ticker: transferable.Tick,
|
||||
Amount: decimals.ToUint256(transferable.Amount, entry.Decimals),
|
||||
Decimals: entry.Decimals,
|
||||
})
|
||||
}
|
||||
|
||||
utxoList = append(utxoList, utxo{
|
||||
TxHash: outPoint.Hash,
|
||||
OutputIndex: outPoint.Index,
|
||||
Extend: utxoExtend{
|
||||
TransferableInscriptions: transferableInscriptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: filter tickers in pg query
|
||||
// filter by req.Id if exists
|
||||
{
|
||||
utxoList = lo.Filter(utxoList, func(u utxo, _ int) bool {
|
||||
for _, transferableInscriptions := range u.Extend.TransferableInscriptions {
|
||||
if ok := strings.EqualFold(req.Id, transferableInscriptions.Ticker); ok {
|
||||
return ok
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
resp := getUTXOsByAddressResponse{
|
||||
Result: &getUTXOsByAddressResult{
|
||||
BlockHeight: blockHeight,
|
||||
List: utxoList,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
18
modules/brc20/api/httphandler/httphandler.go
Normal file
18
modules/brc20/api/httphandler/httphandler.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/usecase"
|
||||
)
|
||||
|
||||
type HttpHandler struct {
|
||||
usecase *usecase.Usecase
|
||||
network common.Network
|
||||
}
|
||||
|
||||
func New(network common.Network, usecase *usecase.Usecase) *HttpHandler {
|
||||
return &HttpHandler{
|
||||
network: network,
|
||||
usecase: usecase,
|
||||
}
|
||||
}
|
||||
19
modules/brc20/api/httphandler/routes.go
Normal file
19
modules/brc20/api/httphandler/routes.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *HttpHandler) Mount(router fiber.Router) error {
|
||||
r := router.Group("/v2/brc20")
|
||||
|
||||
r.Post("/balances/wallet/batch", h.GetBalancesByAddressBatch)
|
||||
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress)
|
||||
r.Get("/transactions", h.GetTransactions)
|
||||
r.Get("/holders/:id", h.GetHolders)
|
||||
r.Get("/info/:id", h.GetTokenInfo)
|
||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOsByAddress)
|
||||
r.Get("/block", h.GetCurrentBlock)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,10 +12,15 @@ import (
|
||||
"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/api/httphandler"
|
||||
"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/modules/brc20/internal/usecase"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcclient"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func New(injector do.Injector) (indexer.IndexerWorker, error) {
|
||||
@@ -66,6 +71,23 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Mount API
|
||||
apiHandlers := lo.Uniq(conf.Modules.BRC20.APIHandlers)
|
||||
for _, handler := range apiHandlers {
|
||||
switch handler { // TODO: support more handlers (e.g. gRPC)
|
||||
case "http":
|
||||
httpServer := do.MustInvoke[*fiber.App](injector)
|
||||
uc := usecase.New(brc20Dg, bitcoinClient)
|
||||
httpHandler := httphandler.New(conf.Network, uc)
|
||||
if err := httpHandler.Mount(httpServer); err != nil {
|
||||
return nil, errors.Wrap(err, "can't mount API")
|
||||
}
|
||||
logger.InfoContext(ctx, "Mounted HTTP handler")
|
||||
default:
|
||||
return nil, errors.Wrapf(errs.Unsupported, "%q API handler is not supported", handler)
|
||||
}
|
||||
}
|
||||
|
||||
indexer := indexer.New(processor, bitcoinDatasource)
|
||||
return indexer, nil
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ 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,
|
||||
"overall_balance" DECIMAL NOT NULL, -- overall balance = available_balance + transferable_balance
|
||||
"available_balance" DECIMAL NOT NULL,
|
||||
PRIMARY KEY ("pkscript", "tick", "block_height")
|
||||
);
|
||||
|
||||
@@ -1,3 +1,121 @@
|
||||
-- name: GetTransferableTransfersByPkScript :many
|
||||
SELECT *
|
||||
FROM "brc20_event_inscribe_transfers"
|
||||
WHERE
|
||||
pkscript = $1
|
||||
AND "brc20_event_inscribe_transfers"."block_height" <= $2
|
||||
AND NOT EXISTS (
|
||||
SELECT NULL
|
||||
FROM "brc20_event_transfer_transfers"
|
||||
WHERE "brc20_event_transfer_transfers"."inscription_id" = "brc20_event_inscribe_transfers"."inscription_id"
|
||||
)
|
||||
ORDER BY "brc20_event_inscribe_transfers"."block_height" DESC;
|
||||
|
||||
-- name: GetBalancesByPkScript :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (tick) * FROM brc20_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY tick, overall_balance DESC
|
||||
)
|
||||
SELECT * FROM balances WHERE overall_balance > 0;
|
||||
|
||||
-- name: GetBalancesByTick :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (pkscript) * FROM brc20_balances WHERE tick = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
|
||||
)
|
||||
SELECT * FROM balances WHERE overall_balance > 0;
|
||||
|
||||
-- name: GetDeployEventByTick :one
|
||||
SELECT * FROM brc20_event_deploys WHERE tick = $1;
|
||||
|
||||
-- name: GetFirstLastInscriptionNumberByTick :one
|
||||
SELECT
|
||||
COALESCE(MIN("inscription_number"), -1)::BIGINT AS "first_inscription_number",
|
||||
COALESCE(MAX("inscription_number"), -1)::BIGINT AS "last_inscription_number"
|
||||
FROM (
|
||||
SELECT inscription_number FROM "brc20_event_mints" WHERE "brc20_event_mints"."tick" = $1
|
||||
UNION ALL
|
||||
SELECT inscription_number FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers"."tick" = $1
|
||||
UNION ALL
|
||||
SELECT inscription_number FROM "brc20_event_transfer_transfers" WHERE "brc20_event_transfer_transfers"."tick" = $1
|
||||
) as events;
|
||||
-- WITH
|
||||
-- "first_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" ASC LIMIT 1),
|
||||
-- "latest_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" DESC LIMIT 1),
|
||||
-- "first_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" ASC LIMIT 1),
|
||||
-- "latest_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" DESC LIMIT 1)
|
||||
-- SELECT
|
||||
-- COALESCE(
|
||||
-- LEAST(
|
||||
-- (SELECT "inscription_number" FROM "first_mint"),
|
||||
-- (SELECT "inscription_number" FROM "first_inscribe_transfer")
|
||||
-- ),
|
||||
-- -1
|
||||
-- ) AS "first_inscription_number",
|
||||
-- COALESCE(
|
||||
-- GREATEST(
|
||||
-- (SELECT "inscription_number" FROM "latest_mint"),
|
||||
-- (SELECT "inscription_number" FROM "latest_inscribe_transfer")
|
||||
-- ),
|
||||
-- -1
|
||||
-- ) AS "last_inscription_number";
|
||||
|
||||
-- name: GetTickEntriesByTicksAndHeight :many
|
||||
WITH "states" AS (
|
||||
-- select latest state
|
||||
SELECT DISTINCT ON ("tick") * FROM "brc20_tick_entry_states" WHERE "tick" = ANY(@ticks::text[]) AND block_height <= @height 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[]) AND deployed_at_height <= @height;;
|
||||
|
||||
-- name: GetDeployEvents :many
|
||||
SELECT * FROM "brc20_event_deploys"
|
||||
WHERE (
|
||||
@filter_pk_script::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = @pk_script
|
||||
) AND (
|
||||
@filter_ticker::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = @ticker
|
||||
) AND (
|
||||
@block_height::INT = 0 OR block_height = @block_height::INT -- if @block_height > 0, apply block_height filter
|
||||
);
|
||||
|
||||
-- name: GetMintEvents :many
|
||||
SELECT * FROM "brc20_event_mints"
|
||||
WHERE (
|
||||
@filter_pk_script::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = @pk_script
|
||||
) AND (
|
||||
@filter_ticker::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = @ticker
|
||||
) AND (
|
||||
@block_height::INT = 0 OR block_height = @block_height::INT -- if @block_height > 0, apply block_height filter
|
||||
);
|
||||
|
||||
-- name: GetInscribeTransferEvents :many
|
||||
SELECT * FROM "brc20_event_inscribe_transfers"
|
||||
WHERE (
|
||||
@filter_pk_script::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = @pk_script
|
||||
) AND (
|
||||
@filter_ticker::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = @ticker
|
||||
) AND (
|
||||
@block_height::INT = 0 OR block_height = @block_height::INT -- if @block_height > 0, apply block_height filter
|
||||
);
|
||||
|
||||
-- name: GetTransferTransferEvents :many
|
||||
SELECT * FROM "brc20_event_transfer_transfers"
|
||||
WHERE (
|
||||
@filter_pk_script::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR from_pkscript = @pk_script
|
||||
OR to_pkscript = @pk_script
|
||||
) AND (
|
||||
@filter_ticker::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = @ticker
|
||||
) AND (
|
||||
@block_height::INT = 0 OR block_height = @block_height::INT -- if @block_height > 0, apply block_height filter
|
||||
);
|
||||
|
||||
-- name: GetLatestIndexedBlock :one
|
||||
SELECT * FROM "brc20_indexed_blocks" ORDER BY "height" DESC LIMIT 1;
|
||||
|
||||
|
||||
@@ -34,6 +34,15 @@ type BRC20ReaderDataGateway interface {
|
||||
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)
|
||||
GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error)
|
||||
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error)
|
||||
GetTransferableTransfersByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.EventInscribeTransfer, error)
|
||||
GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error)
|
||||
GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (first, last int64, err error)
|
||||
GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error)
|
||||
GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error)
|
||||
GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error)
|
||||
GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error)
|
||||
}
|
||||
|
||||
type BRC20WriterDataGateway interface {
|
||||
|
||||
@@ -2,6 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@@ -231,6 +232,179 @@ func (r *Repository) GetTickEntriesByTicks(ctx context.Context, ticks []string)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error) {
|
||||
models, err := r.queries.GetBalancesByTick(ctx, gen.GetBalancesByTickParams{
|
||||
Tick: tick,
|
||||
BlockHeight: int32(blockHeight),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.Balance, 0, len(models))
|
||||
for _, model := range models {
|
||||
balance, err := mapBalanceModelToType(gen.Brc20Balance(model))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse balance model")
|
||||
}
|
||||
result = append(result, &balance)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error) {
|
||||
models, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{
|
||||
Pkscript: hex.EncodeToString(pkScript),
|
||||
BlockHeight: int32(blockHeight),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make(map[string]*entity.Balance)
|
||||
for _, model := range models {
|
||||
balance, err := mapBalanceModelToType(gen.Brc20Balance(model))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse balance model")
|
||||
}
|
||||
result[balance.Tick] = &balance
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetTransferableTransfersByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.EventInscribeTransfer, error) {
|
||||
models, err := r.queries.GetTransferableTransfersByPkScript(ctx, gen.GetTransferableTransfersByPkScriptParams{
|
||||
Pkscript: hex.EncodeToString(pkScript),
|
||||
BlockHeight: int32(blockHeight),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.EventInscribeTransfer, 0, len(models))
|
||||
for _, model := range models {
|
||||
ent, err := mapEventInscribeTransferModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
result = append(result, &ent)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error) {
|
||||
model, err := r.queries.GetDeployEventByTick(ctx, tick)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ent, err := mapEventDeployModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
return &ent, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (first, last int64, err error) {
|
||||
model, err := r.queries.GetFirstLastInscriptionNumberByTick(ctx, tick)
|
||||
if err != nil {
|
||||
return -1, -1, errors.WithStack(err)
|
||||
}
|
||||
return model.FirstInscriptionNumber, model.LastInscriptionNumber, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error) {
|
||||
models, err := r.queries.GetDeployEvents(ctx, gen.GetDeployEventsParams{
|
||||
FilterPkScript: pkScript != nil,
|
||||
PkScript: hex.EncodeToString(pkScript),
|
||||
FilterTicker: tick != "",
|
||||
Ticker: tick,
|
||||
BlockHeight: int32(height),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.EventDeploy, 0, len(models))
|
||||
for _, model := range models {
|
||||
ent, err := mapEventDeployModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
result = append(result, &ent)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error) {
|
||||
models, err := r.queries.GetMintEvents(ctx, gen.GetMintEventsParams{
|
||||
FilterPkScript: pkScript != nil,
|
||||
PkScript: hex.EncodeToString(pkScript),
|
||||
FilterTicker: tick != "",
|
||||
Ticker: tick,
|
||||
BlockHeight: int32(height),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.EventMint, 0, len(models))
|
||||
for _, model := range models {
|
||||
ent, err := mapEventMintModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
result = append(result, &ent)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error) {
|
||||
models, err := r.queries.GetInscribeTransferEvents(ctx, gen.GetInscribeTransferEventsParams{
|
||||
FilterPkScript: pkScript != nil,
|
||||
PkScript: hex.EncodeToString(pkScript),
|
||||
FilterTicker: tick != "",
|
||||
Ticker: tick,
|
||||
BlockHeight: int32(height),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.EventInscribeTransfer, 0, len(models))
|
||||
for _, model := range models {
|
||||
ent, err := mapEventInscribeTransferModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
result = append(result, &ent)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error) {
|
||||
models, err := r.queries.GetTransferTransferEvents(ctx, gen.GetTransferTransferEventsParams{
|
||||
FilterPkScript: pkScript != nil,
|
||||
PkScript: hex.EncodeToString(pkScript),
|
||||
FilterTicker: tick != "",
|
||||
Ticker: tick,
|
||||
BlockHeight: int32(height),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result := make([]*entity.EventTransferTransfer, 0, len(models))
|
||||
for _, model := range models {
|
||||
ent, err := mapEventTransferTransferModelToType(model)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse event model")
|
||||
}
|
||||
result = append(result, &ent)
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -203,6 +203,188 @@ func (q *Queries) GetBalancesBatchAtHeight(ctx context.Context, arg GetBalancesB
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getBalancesByPkScript = `-- name: GetBalancesByPkScript :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (tick) pkscript, block_height, tick, overall_balance, available_balance FROM brc20_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY tick, overall_balance DESC
|
||||
)
|
||||
SELECT pkscript, block_height, tick, overall_balance, available_balance FROM balances WHERE overall_balance > 0
|
||||
`
|
||||
|
||||
type GetBalancesByPkScriptParams struct {
|
||||
Pkscript string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
type GetBalancesByPkScriptRow struct {
|
||||
Pkscript string
|
||||
BlockHeight int32
|
||||
Tick string
|
||||
OverallBalance pgtype.Numeric
|
||||
AvailableBalance pgtype.Numeric
|
||||
}
|
||||
|
||||
func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) {
|
||||
rows, err := q.db.Query(ctx, getBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetBalancesByPkScriptRow
|
||||
for rows.Next() {
|
||||
var i GetBalancesByPkScriptRow
|
||||
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 getBalancesByTick = `-- name: GetBalancesByTick :many
|
||||
WITH balances AS (
|
||||
SELECT DISTINCT ON (pkscript) pkscript, block_height, tick, overall_balance, available_balance FROM brc20_balances WHERE tick = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
|
||||
)
|
||||
SELECT pkscript, block_height, tick, overall_balance, available_balance FROM balances WHERE overall_balance > 0
|
||||
`
|
||||
|
||||
type GetBalancesByTickParams struct {
|
||||
Tick string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
type GetBalancesByTickRow struct {
|
||||
Pkscript string
|
||||
BlockHeight int32
|
||||
Tick string
|
||||
OverallBalance pgtype.Numeric
|
||||
AvailableBalance pgtype.Numeric
|
||||
}
|
||||
|
||||
func (q *Queries) GetBalancesByTick(ctx context.Context, arg GetBalancesByTickParams) ([]GetBalancesByTickRow, error) {
|
||||
rows, err := q.db.Query(ctx, getBalancesByTick, arg.Tick, arg.BlockHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetBalancesByTickRow
|
||||
for rows.Next() {
|
||||
var i GetBalancesByTickRow
|
||||
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 getDeployEventByTick = `-- name: GetDeployEventByTick :one
|
||||
SELECT id, 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 FROM brc20_event_deploys WHERE tick = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetDeployEventByTick(ctx context.Context, tick string) (Brc20EventDeploy, error) {
|
||||
row := q.db.QueryRow(ctx, getDeployEventByTick, tick)
|
||||
var i Brc20EventDeploy
|
||||
err := row.Scan(
|
||||
&i.Id,
|
||||
&i.InscriptionID,
|
||||
&i.InscriptionNumber,
|
||||
&i.Tick,
|
||||
&i.OriginalTick,
|
||||
&i.TxHash,
|
||||
&i.BlockHeight,
|
||||
&i.TxIndex,
|
||||
&i.Timestamp,
|
||||
&i.Pkscript,
|
||||
&i.Satpoint,
|
||||
&i.TotalSupply,
|
||||
&i.Decimals,
|
||||
&i.LimitPerMint,
|
||||
&i.IsSelfMint,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDeployEvents = `-- name: GetDeployEvents :many
|
||||
SELECT id, 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 FROM "brc20_event_deploys"
|
||||
WHERE (
|
||||
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = $2
|
||||
) AND (
|
||||
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = $4
|
||||
) AND (
|
||||
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
|
||||
)
|
||||
`
|
||||
|
||||
type GetDeployEventsParams struct {
|
||||
FilterPkScript bool
|
||||
PkScript string
|
||||
FilterTicker bool
|
||||
Ticker string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetDeployEvents(ctx context.Context, arg GetDeployEventsParams) ([]Brc20EventDeploy, error) {
|
||||
rows, err := q.db.Query(ctx, getDeployEvents,
|
||||
arg.FilterPkScript,
|
||||
arg.PkScript,
|
||||
arg.FilterTicker,
|
||||
arg.Ticker,
|
||||
arg.BlockHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Brc20EventDeploy
|
||||
for rows.Next() {
|
||||
var i Brc20EventDeploy
|
||||
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.TotalSupply,
|
||||
&i.Decimals,
|
||||
&i.LimitPerMint,
|
||||
&i.IsSelfMint,
|
||||
); 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[])
|
||||
`
|
||||
@@ -242,6 +424,31 @@ func (q *Queries) GetEventInscribeTransfersByInscriptionIds(ctx context.Context,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFirstLastInscriptionNumberByTick = `-- name: GetFirstLastInscriptionNumberByTick :one
|
||||
SELECT
|
||||
COALESCE(MIN("inscription_number"), -1)::BIGINT AS "first_inscription_number",
|
||||
COALESCE(MAX("inscription_number"), -1)::BIGINT AS "last_inscription_number"
|
||||
FROM (
|
||||
SELECT inscription_number FROM "brc20_event_mints" WHERE "brc20_event_mints"."tick" = $1
|
||||
UNION ALL
|
||||
SELECT inscription_number FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers"."tick" = $1
|
||||
UNION ALL
|
||||
SELECT inscription_number FROM "brc20_event_transfer_transfers" WHERE "brc20_event_transfer_transfers"."tick" = $1
|
||||
) as events
|
||||
`
|
||||
|
||||
type GetFirstLastInscriptionNumberByTickRow struct {
|
||||
FirstInscriptionNumber int64
|
||||
LastInscriptionNumber int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (GetFirstLastInscriptionNumberByTickRow, error) {
|
||||
row := q.db.QueryRow(ctx, getFirstLastInscriptionNumberByTick, tick)
|
||||
var i GetFirstLastInscriptionNumberByTickRow
|
||||
err := row.Scan(&i.FirstInscriptionNumber, &i.LastInscriptionNumber)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getIndexedBlockByHeight = `-- name: GetIndexedBlockByHeight :one
|
||||
SELECT height, hash, event_hash, cumulative_event_hash FROM "brc20_indexed_blocks" WHERE "height" = $1
|
||||
`
|
||||
@@ -258,6 +465,68 @@ func (q *Queries) GetIndexedBlockByHeight(ctx context.Context, height int32) (Br
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getInscribeTransferEvents = `-- name: GetInscribeTransferEvents :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 (
|
||||
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = $2
|
||||
) AND (
|
||||
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = $4
|
||||
) AND (
|
||||
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
|
||||
)
|
||||
`
|
||||
|
||||
type GetInscribeTransferEventsParams struct {
|
||||
FilterPkScript bool
|
||||
PkScript string
|
||||
FilterTicker bool
|
||||
Ticker string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetInscribeTransferEvents(ctx context.Context, arg GetInscribeTransferEventsParams) ([]Brc20EventInscribeTransfer, error) {
|
||||
rows, err := q.db.Query(ctx, getInscribeTransferEvents,
|
||||
arg.FilterPkScript,
|
||||
arg.PkScript,
|
||||
arg.FilterTicker,
|
||||
arg.Ticker,
|
||||
arg.BlockHeight,
|
||||
)
|
||||
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 getInscriptionEntriesByIds = `-- name: GetInscriptionEntriesByIds :many
|
||||
WITH "states" AS (
|
||||
-- select latest state
|
||||
@@ -532,6 +801,67 @@ func (q *Queries) GetLatestProcessorStats(ctx context.Context) (Brc20ProcessorSt
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMintEvents = `-- name: GetMintEvents :many
|
||||
SELECT id, inscription_id, inscription_number, tick, original_tick, tx_hash, block_height, tx_index, timestamp, pkscript, satpoint, amount, parent_id FROM "brc20_event_mints"
|
||||
WHERE (
|
||||
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR pkscript = $2
|
||||
) AND (
|
||||
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = $4
|
||||
) AND (
|
||||
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
|
||||
)
|
||||
`
|
||||
|
||||
type GetMintEventsParams struct {
|
||||
FilterPkScript bool
|
||||
PkScript string
|
||||
FilterTicker bool
|
||||
Ticker string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetMintEvents(ctx context.Context, arg GetMintEventsParams) ([]Brc20EventMint, error) {
|
||||
rows, err := q.db.Query(ctx, getMintEvents,
|
||||
arg.FilterPkScript,
|
||||
arg.PkScript,
|
||||
arg.FilterTicker,
|
||||
arg.Ticker,
|
||||
arg.BlockHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Brc20EventMint
|
||||
for rows.Next() {
|
||||
var i Brc20EventMint
|
||||
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.Amount,
|
||||
&i.ParentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTickEntriesByTicks = `-- name: GetTickEntriesByTicks :many
|
||||
WITH "states" AS (
|
||||
-- select latest state
|
||||
@@ -595,3 +925,214 @@ func (q *Queries) GetTickEntriesByTicks(ctx context.Context, ticks []string) ([]
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTickEntriesByTicksAndHeight = `-- name: GetTickEntriesByTicksAndHeight :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[]) AND block_height <= $2 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[]) AND deployed_at_height <= $2
|
||||
`
|
||||
|
||||
type GetTickEntriesByTicksAndHeightParams struct {
|
||||
Ticks []string
|
||||
Height int32
|
||||
}
|
||||
|
||||
type GetTickEntriesByTicksAndHeightRow 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
|
||||
}
|
||||
|
||||
// WITH
|
||||
// "first_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" ASC LIMIT 1),
|
||||
// "latest_mint" AS (SELECT "inscription_number" FROM "brc20_event_mints" WHERE "brc20_event_mints".tick = $1 ORDER BY "id" DESC LIMIT 1),
|
||||
// "first_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" ASC LIMIT 1),
|
||||
// "latest_inscribe_transfer" AS (SELECT "inscription_number" FROM "brc20_event_inscribe_transfers" WHERE "brc20_event_inscribe_transfers".tick = $1 ORDER BY "id" DESC LIMIT 1)
|
||||
// SELECT
|
||||
//
|
||||
// COALESCE(
|
||||
// LEAST(
|
||||
// (SELECT "inscription_number" FROM "first_mint"),
|
||||
// (SELECT "inscription_number" FROM "first_inscribe_transfer")
|
||||
// ),
|
||||
// -1
|
||||
// ) AS "first_inscription_number",
|
||||
// COALESCE(
|
||||
// GREATEST(
|
||||
// (SELECT "inscription_number" FROM "latest_mint"),
|
||||
// (SELECT "inscription_number" FROM "latest_inscribe_transfer")
|
||||
// ),
|
||||
// -1
|
||||
// ) AS "last_inscription_number";
|
||||
func (q *Queries) GetTickEntriesByTicksAndHeight(ctx context.Context, arg GetTickEntriesByTicksAndHeightParams) ([]GetTickEntriesByTicksAndHeightRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTickEntriesByTicksAndHeight, arg.Ticks, arg.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTickEntriesByTicksAndHeightRow
|
||||
for rows.Next() {
|
||||
var i GetTickEntriesByTicksAndHeightRow
|
||||
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
|
||||
}
|
||||
|
||||
const getTransferTransferEvents = `-- name: GetTransferTransferEvents :many
|
||||
SELECT id, 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 FROM "brc20_event_transfer_transfers"
|
||||
WHERE (
|
||||
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
|
||||
OR from_pkscript = $2
|
||||
OR to_pkscript = $2
|
||||
) AND (
|
||||
$3::BOOLEAN = FALSE -- if @filter_ticker is TRUE, apply ticker filter
|
||||
OR tick = $4
|
||||
) AND (
|
||||
$5::INT = 0 OR block_height = $5::INT -- if @block_height > 0, apply block_height filter
|
||||
)
|
||||
`
|
||||
|
||||
type GetTransferTransferEventsParams struct {
|
||||
FilterPkScript bool
|
||||
PkScript string
|
||||
FilterTicker bool
|
||||
Ticker string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetTransferTransferEvents(ctx context.Context, arg GetTransferTransferEventsParams) ([]Brc20EventTransferTransfer, error) {
|
||||
rows, err := q.db.Query(ctx, getTransferTransferEvents,
|
||||
arg.FilterPkScript,
|
||||
arg.PkScript,
|
||||
arg.FilterTicker,
|
||||
arg.Ticker,
|
||||
arg.BlockHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Brc20EventTransferTransfer
|
||||
for rows.Next() {
|
||||
var i Brc20EventTransferTransfer
|
||||
if err := rows.Scan(
|
||||
&i.Id,
|
||||
&i.InscriptionID,
|
||||
&i.InscriptionNumber,
|
||||
&i.Tick,
|
||||
&i.OriginalTick,
|
||||
&i.TxHash,
|
||||
&i.BlockHeight,
|
||||
&i.TxIndex,
|
||||
&i.Timestamp,
|
||||
&i.FromPkscript,
|
||||
&i.FromSatpoint,
|
||||
&i.FromInputIndex,
|
||||
&i.ToPkscript,
|
||||
&i.ToSatpoint,
|
||||
&i.ToOutputIndex,
|
||||
&i.SpentAsFee,
|
||||
&i.Amount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTransferableTransfersByPkScript = `-- name: GetTransferableTransfersByPkScript :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
|
||||
pkscript = $1
|
||||
AND "brc20_event_inscribe_transfers"."block_height" <= $2
|
||||
AND NOT EXISTS (
|
||||
SELECT NULL
|
||||
FROM "brc20_event_transfer_transfers"
|
||||
WHERE "brc20_event_transfer_transfers"."inscription_id" = "brc20_event_inscribe_transfers"."inscription_id"
|
||||
)
|
||||
ORDER BY "brc20_event_inscribe_transfers"."block_height" DESC
|
||||
`
|
||||
|
||||
type GetTransferableTransfersByPkScriptParams struct {
|
||||
Pkscript string
|
||||
BlockHeight int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetTransferableTransfersByPkScript(ctx context.Context, arg GetTransferableTransfersByPkScriptParams) ([]Brc20EventInscribeTransfer, error) {
|
||||
rows, err := q.db.Query(ctx, getTransferableTransfersByPkScript, arg.Pkscript, arg.BlockHeight)
|
||||
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
|
||||
}
|
||||
|
||||
24
modules/brc20/internal/usecase/get_balances.go
Normal file
24
modules/brc20/internal/usecase/get_balances.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[string]*entity.Balance, error) {
|
||||
balances, err := u.dg.GetBalancesByPkScript(ctx, pkScript, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
|
||||
}
|
||||
return balances, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetBalancesByTick(ctx context.Context, tick string, blockHeight uint64) ([]*entity.Balance, error) {
|
||||
balances, err := u.dg.GetBalancesByTick(ctx, tick, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get balance by tick")
|
||||
}
|
||||
return balances, nil
|
||||
}
|
||||
33
modules/brc20/internal/usecase/get_entry.go
Normal file
33
modules/brc20/internal/usecase/get_entry.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetTickEntryByTickBatch(ctx context.Context, ticks []string) (map[string]*entity.TickEntry, error) {
|
||||
entries, err := u.dg.GetTickEntriesByTicks(ctx, ticks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetTickEntriesByTicks")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetTickEntryByTickAndHeight(ctx context.Context, tick string, blockHeight uint64) (*entity.TickEntry, error) {
|
||||
entries, err := u.GetTickEntryByTickAndHeightBatch(ctx, []string{tick}, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
entry, ok := entries[tick]
|
||||
if !ok {
|
||||
return nil, errors.Wrap(errs.NotFound, "entry not found")
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetTickEntryByTickAndHeightBatch(ctx context.Context, ticks []string, blockHeight uint64) (map[string]*entity.TickEntry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
15
modules/brc20/internal/usecase/get_inscription_number.go
Normal file
15
modules/brc20/internal/usecase/get_inscription_number.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetFirstLastInscriptionNumberByTick(ctx context.Context, tick string) (int64, int64, error) {
|
||||
first, last, err := u.dg.GetFirstLastInscriptionNumberByTick(ctx, tick)
|
||||
if err != nil {
|
||||
return -1, -1, errors.Wrap(err, "error during GetFirstLastInscriptionNumberByTick")
|
||||
}
|
||||
return first, last, nil
|
||||
}
|
||||
16
modules/brc20/internal/usecase/get_latest_block.go
Normal file
16
modules/brc20/internal/usecase/get_latest_block.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetLatestBlock(ctx context.Context) (types.BlockHeader, error) {
|
||||
blockHeader, err := u.dg.GetLatestBlock(ctx)
|
||||
if err != nil {
|
||||
return types.BlockHeader{}, errors.Wrap(err, "failed to get latest block")
|
||||
}
|
||||
return blockHeader, nil
|
||||
}
|
||||
16
modules/brc20/internal/usecase/get_tick_events.go
Normal file
16
modules/brc20/internal/usecase/get_tick_events.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetDeployEventByTick(ctx context.Context, tick string) (*entity.EventDeploy, error) {
|
||||
result, err := u.dg.GetDeployEventByTick(ctx, tick)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetDeployEventByTick")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
40
modules/brc20/internal/usecase/get_transactions.go
Normal file
40
modules/brc20/internal/usecase/get_transactions.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetDeployEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventDeploy, error) {
|
||||
result, err := u.dg.GetDeployEvents(ctx, pkScript, tick, height)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetDeployEvents")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetMintEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventMint, error) {
|
||||
result, err := u.dg.GetMintEvents(ctx, pkScript, tick, height)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetMintEvents")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetInscribeTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventInscribeTransfer, error) {
|
||||
result, err := u.dg.GetInscribeTransferEvents(ctx, pkScript, tick, height)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetInscribeTransferEvents")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetTransferTransferEvents(ctx context.Context, pkScript []byte, tick string, height uint64) ([]*entity.EventTransferTransfer, error) {
|
||||
result, err := u.dg.GetTransferTransferEvents(ctx, pkScript, tick, height)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetTransferTransfersEvents")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetTransferableTransfersByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.EventInscribeTransfer, error) {
|
||||
result, err := u.dg.GetTransferableTransfersByPkScript(ctx, pkScript, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetTransferableTransfersByPkScript")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
18
modules/brc20/internal/usecase/usecase.go
Normal file
18
modules/brc20/internal/usecase/usecase.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcclient"
|
||||
)
|
||||
|
||||
type Usecase struct {
|
||||
dg datagateway.BRC20DataGateway
|
||||
bitcoinClient btcclient.Contract
|
||||
}
|
||||
|
||||
func New(dg datagateway.BRC20DataGateway, bitcoinClient btcclient.Contract) *Usecase {
|
||||
return &Usecase{
|
||||
dg: dg,
|
||||
bitcoinClient: bitcoinClient,
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ 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
|
||||
@@ -40,13 +41,14 @@ type Processor struct {
|
||||
newRuneTxs []*entity.RuneTransaction
|
||||
}
|
||||
|
||||
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, network common.Network, reportingClient *reportingclient.ReportingClient) *Processor {
|
||||
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, network common.Network, reportingClient *reportingclient.ReportingClient, cleanupFuncs []func(context.Context) error) *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),
|
||||
@@ -228,3 +230,13 @@ 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...))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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)
|
||||
@@ -42,7 +43,10 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
|
||||
}
|
||||
return nil, errors.Wrap(err, "can't create Postgres connection pool")
|
||||
}
|
||||
defer pg.Close()
|
||||
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
|
||||
pg.Close()
|
||||
return nil
|
||||
})
|
||||
runesRepo := runespostgres.NewRepository(pg)
|
||||
runesDg = runesRepo
|
||||
indexerInfoDg = runesRepo
|
||||
@@ -62,7 +66,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)
|
||||
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient, cleanupFuncs)
|
||||
if err := processor.VerifyStates(ctx); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
203
pkg/btcutils/address.go
Normal file
203
pkg/btcutils/address.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
)
|
||||
|
||||
// IsAddress returns whether or not the passed string is a valid bitcoin address and valid supported type.
|
||||
//
|
||||
// NetParams is optional. If provided, we only check for that network,
|
||||
// otherwise, we check for all supported networks.
|
||||
func IsAddress(address string, defaultNet ...*chaincfg.Params) bool {
|
||||
if len(address) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If defaultNet is provided, we only check for that network.
|
||||
net, ok := utils.Optional(defaultNet)
|
||||
if ok {
|
||||
_, _, err := parseAddress(address, net)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Otherwise, we check for all supported networks.
|
||||
for _, net := range supportedNetworks {
|
||||
_, _, err := parseAddress(address, net)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: create GetAddressNetwork
|
||||
// check `Bech32HRPSegwit` prefix or netID for P2SH/P2PKH is equal to `PubKeyHashAddrID/ScriptHashAddrID`
|
||||
|
||||
// GetAddressType returns the address type of the passed address.
|
||||
func GetAddressType(address string, net *chaincfg.Params) (AddressType, error) {
|
||||
_, addrType, err := parseAddress(address, net)
|
||||
return addrType, errors.WithStack(err)
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
decoded btcutil.Address
|
||||
net *chaincfg.Params
|
||||
encoded string
|
||||
encodedType AddressType
|
||||
scriptPubKey []byte
|
||||
}
|
||||
|
||||
// NewAddress creates a new address from the given address string.
|
||||
//
|
||||
// defaultNet is required if your address is P2SH or P2PKH (legacy or nested segwit)
|
||||
// If your address is P2WSH, P2WPKH or P2TR, defaultNet is not required.
|
||||
func NewAddress(address string, defaultNet ...*chaincfg.Params) Address {
|
||||
addr, err := SafeNewAddress(address, defaultNet...)
|
||||
if err != nil {
|
||||
logger.Panic("can't create parse address", slogx.Error(err), slogx.String("package", "btcutils"))
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// SafeNewAddress creates a new address from the given address string.
|
||||
// It returns an error if the address is invalid.
|
||||
//
|
||||
// defaultNet is required if your address is P2SH or P2PKH (legacy or nested segwit)
|
||||
// If your address is P2WSH, P2WPKH or P2TR, defaultNet is not required.
|
||||
func SafeNewAddress(address string, defaultNet ...*chaincfg.Params) (Address, error) {
|
||||
net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams)
|
||||
|
||||
decoded, addrType, err := parseAddress(address, net)
|
||||
if err != nil {
|
||||
return Address{}, errors.Wrap(err, "can't parse address")
|
||||
}
|
||||
|
||||
scriptPubkey, err := txscript.PayToAddrScript(decoded)
|
||||
if err != nil {
|
||||
return Address{}, errors.Wrap(err, "can't get script pubkey")
|
||||
}
|
||||
|
||||
return Address{
|
||||
decoded: decoded,
|
||||
net: net,
|
||||
encoded: decoded.EncodeAddress(),
|
||||
encodedType: addrType,
|
||||
scriptPubKey: scriptPubkey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns the address string.
|
||||
func (a Address) String() string {
|
||||
return a.encoded
|
||||
}
|
||||
|
||||
// Type returns the address type.
|
||||
func (a Address) Type() AddressType {
|
||||
return a.encodedType
|
||||
}
|
||||
|
||||
// Decoded returns the btcutil.Address
|
||||
func (a Address) Decoded() btcutil.Address {
|
||||
return a.decoded
|
||||
}
|
||||
|
||||
// IsForNet returns whether or not the address is associated with the passed bitcoin network.
|
||||
func (a Address) IsForNet(net *chaincfg.Params) bool {
|
||||
return a.decoded.IsForNet(net)
|
||||
}
|
||||
|
||||
// ScriptAddress returns the raw bytes of the address to be used when inserting the address into a txout's script.
|
||||
func (a Address) ScriptAddress() []byte {
|
||||
return a.decoded.ScriptAddress()
|
||||
}
|
||||
|
||||
// Net returns the address network params.
|
||||
func (a Address) Net() *chaincfg.Params {
|
||||
return a.net
|
||||
}
|
||||
|
||||
// NetworkName
|
||||
func (a Address) NetworkName() string {
|
||||
return a.net.Name
|
||||
}
|
||||
|
||||
// ScriptPubKey or pubkey script
|
||||
func (a Address) ScriptPubKey() []byte {
|
||||
return a.scriptPubKey
|
||||
}
|
||||
|
||||
// Equal return true if addresses are equal
|
||||
func (a Address) Equal(b Address) bool {
|
||||
return a.encoded == b.encoded
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface.
|
||||
func (a Address) MarshalText() ([]byte, error) {
|
||||
return []byte(a.encoded), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
func (a *Address) UnmarshalText(input []byte) error {
|
||||
address := string(input)
|
||||
addr, err := SafeNewAddress(address)
|
||||
if err == nil {
|
||||
*a = addr
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(errs.InvalidArgument, "invalid address `%s`", address)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (a Address) MarshalJSON() ([]byte, error) {
|
||||
t, err := a.MarshalText()
|
||||
if err != nil {
|
||||
return nil, &json.MarshalerError{Type: reflect.TypeOf(a), Err: err}
|
||||
}
|
||||
b := make([]byte, len(t)+2)
|
||||
b[0], b[len(b)-1] = '"', '"' // add quotes
|
||||
copy(b[1:], t)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a hash in hex syntax.
|
||||
func (a *Address) UnmarshalJSON(input []byte) error {
|
||||
if !(len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"') {
|
||||
return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeOf(Address{})}
|
||||
}
|
||||
if err := a.UnmarshalText(input[1 : len(input)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAddress(address string, params *chaincfg.Params) (btcutil.Address, AddressType, error) {
|
||||
decoded, err := btcutil.DecodeAddress(address, params)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "can't decode address `%s` for network `%s`", address, params.Name)
|
||||
}
|
||||
|
||||
switch decoded.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
return decoded, AddressP2WPKH, nil
|
||||
case *btcutil.AddressTaproot:
|
||||
return decoded, AddressP2TR, nil
|
||||
case *btcutil.AddressScriptHash:
|
||||
return decoded, AddressP2SH, nil
|
||||
case *btcutil.AddressPubKeyHash:
|
||||
return decoded, AddressP2PKH, nil
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
return decoded, AddressP2WSH, nil
|
||||
default:
|
||||
return nil, 0, errors.Wrap(errs.Unsupported, "unsupported address type")
|
||||
}
|
||||
}
|
||||
80
pkg/btcutils/address_bench_test.go
Normal file
80
pkg/btcutils/address_bench_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package btcutils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
)
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
|
||||
# Compare this benchmark to go-ethereum/common.Address utils
|
||||
- go-ethereum/common.HexToAddress speed: 45 ns/op, 48 B/op, 1 allocs/op
|
||||
- go-ethereum/common.IsHexAddress speed: 25 ns/op, 0 B/op, 0 allocs/op
|
||||
|
||||
It's slower than go-ethereum/common.Address utils because ethereum wallet address is Hex string 20 bytes,
|
||||
but Bitcoin has many types of address and each type has complex algorithm to solve (can't solve and validate address type directly from address string)
|
||||
|
||||
20/Jan/2024 @Planxnx Macbook Air M1 16GB
|
||||
BenchmarkIsAddress/specific-network/mainnet/P2WPKH-8 1776146 625.6 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/testnet3/P2WPKH-8 1917876 623.2 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/mainnet/P2TR-8 1330348 915.4 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/testnet3/P2TR-8 1235806 931.1 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/mainnet/P2WSH-8 1261730 960.9 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/testnet3/P2WSH-8 1307851 916.1 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/specific-network/mainnet/P2SH-8 3081762 402.0 ns/op 192 B/op 8 allocs/op
|
||||
BenchmarkIsAddress/specific-network/testnet3/P2SH-8 3245838 344.9 ns/op 176 B/op 7 allocs/op
|
||||
BenchmarkIsAddress/specific-network/mainnet/P2PKH-8 2904252 410.4 ns/op 184 B/op 8 allocs/op
|
||||
BenchmarkIsAddress/specific-network/testnet3/P2PKH-8 3522332 342.8 ns/op 176 B/op 7 allocs/op
|
||||
BenchmarkIsAddress/automate-network/mainnet/P2WPKH-8 1882059 637.6 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/testnet3/P2WPKH-8 1626151 664.8 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/mainnet/P2TR-8 1250253 952.1 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/testnet3/P2TR-8 1257901 993.7 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/mainnet/P2WSH-8 1000000 1005 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/testnet3/P2WSH-8 1209108 971.2 ns/op 160 B/op 3 allocs/op
|
||||
BenchmarkIsAddress/automate-network/mainnet/P2SH-8 1869075 625.0 ns/op 268 B/op 9 allocs/op
|
||||
BenchmarkIsAddress/automate-network/testnet3/P2SH-8 779496 1609 ns/op 694 B/op 17 allocs/op
|
||||
BenchmarkIsAddress/automate-network/mainnet/P2PKH-8 1924058 650.6 ns/op 259 B/op 9 allocs/op
|
||||
BenchmarkIsAddress/automate-network/testnet3/P2PKH-8 721510 1690 ns/op 694 B/op 17 allocs/op
|
||||
*/
|
||||
func BenchmarkIsAddress(b *testing.B) {
|
||||
cases := []btcutils.Address{
|
||||
/* P2WPKH */ btcutils.NewAddress("bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh", &chaincfg.MainNetParams),
|
||||
/* P2WPKH */ btcutils.NewAddress("tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey", &chaincfg.TestNet3Params),
|
||||
/* P2TR */ btcutils.NewAddress("bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38", &chaincfg.MainNetParams),
|
||||
/* P2TR */ btcutils.NewAddress("tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg", &chaincfg.TestNet3Params),
|
||||
/* P2WSH */ btcutils.NewAddress("bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", &chaincfg.MainNetParams),
|
||||
/* P2WSH */ btcutils.NewAddress("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", &chaincfg.TestNet3Params),
|
||||
/* P2SH */ btcutils.NewAddress("3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", &chaincfg.MainNetParams),
|
||||
/* P2SH */ btcutils.NewAddress("2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ", &chaincfg.TestNet3Params),
|
||||
/* P2PKH */ btcutils.NewAddress("1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", &chaincfg.MainNetParams),
|
||||
/* P2PKH */ btcutils.NewAddress("migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3", &chaincfg.TestNet3Params),
|
||||
}
|
||||
|
||||
b.Run("specific-network", func(b *testing.B) {
|
||||
for _, c := range cases {
|
||||
b.Run(c.NetworkName()+"/"+c.Type().String(), func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = btcutils.IsAddress(c.String(), c.Net())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("automate-network", func(b *testing.B) {
|
||||
for _, c := range cases {
|
||||
b.Run(c.NetworkName()+"/"+c.Type().String(), func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ok := btcutils.IsAddress(c.String())
|
||||
if !ok {
|
||||
b.Error("IsAddress returned false")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
363
pkg/btcutils/address_test.go
Normal file
363
pkg/btcutils/address_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package btcutils_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAddressType(t *testing.T) {
|
||||
type Spec struct {
|
||||
Address string
|
||||
DefaultNet *chaincfg.Params
|
||||
|
||||
ExpectedError error
|
||||
ExpectedAddressType btcutils.AddressType
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WPKH,
|
||||
},
|
||||
{
|
||||
Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WPKH,
|
||||
},
|
||||
{
|
||||
Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2TR,
|
||||
},
|
||||
{
|
||||
Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2TR,
|
||||
},
|
||||
{
|
||||
Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2SH,
|
||||
},
|
||||
{
|
||||
Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2PKH,
|
||||
},
|
||||
{
|
||||
Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WSH,
|
||||
},
|
||||
{
|
||||
Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3",
|
||||
DefaultNet: &chaincfg.TestNet3Params,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2PKH,
|
||||
},
|
||||
{
|
||||
Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WSH,
|
||||
},
|
||||
{
|
||||
Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ",
|
||||
DefaultNet: &chaincfg.TestNet3Params,
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2SH,
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) {
|
||||
actualAddressType, actualError := btcutils.GetAddressType(spec.Address, spec.DefaultNet)
|
||||
if spec.ExpectedError != nil {
|
||||
assert.ErrorIs(t, actualError, spec.ExpectedError)
|
||||
} else {
|
||||
assert.Equal(t, spec.ExpectedAddressType, actualAddressType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAddress(t *testing.T) {
|
||||
type Spec struct {
|
||||
Address string
|
||||
DefaultNet *chaincfg.Params
|
||||
|
||||
ExpectedAddressType btcutils.AddressType
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2WPKH,
|
||||
},
|
||||
{
|
||||
Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2WPKH,
|
||||
},
|
||||
{
|
||||
Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2TR,
|
||||
},
|
||||
{
|
||||
Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2TR,
|
||||
},
|
||||
{
|
||||
Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2WSH,
|
||||
},
|
||||
{
|
||||
Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
|
||||
// DefaultNet: &chaincfg.MainNetParams, // Optional
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2WSH,
|
||||
},
|
||||
{
|
||||
Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2SH,
|
||||
},
|
||||
{
|
||||
Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ",
|
||||
DefaultNet: &chaincfg.TestNet3Params,
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2SH,
|
||||
},
|
||||
{
|
||||
Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2PKH,
|
||||
},
|
||||
{
|
||||
Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3",
|
||||
DefaultNet: &chaincfg.TestNet3Params,
|
||||
|
||||
ExpectedAddressType: btcutils.AddressP2PKH,
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("address:%s,type:%s", spec.Address, spec.ExpectedAddressType), func(t *testing.T) {
|
||||
addr := btcutils.NewAddress(spec.Address, spec.DefaultNet)
|
||||
|
||||
assert.Equal(t, spec.ExpectedAddressType, addr.Type())
|
||||
assert.Equal(t, spec.Address, addr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAddress(t *testing.T) {
|
||||
type Spec struct {
|
||||
Address string
|
||||
Expected bool
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3",
|
||||
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "",
|
||||
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz2",
|
||||
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczz",
|
||||
|
||||
Expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) {
|
||||
ok := btcutils.IsAddress(spec.Address)
|
||||
assert.Equal(t, spec.Expected, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressEncoding(t *testing.T) {
|
||||
rawAddress := "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh"
|
||||
address := btcutils.NewAddress(rawAddress, &chaincfg.MainNetParams)
|
||||
|
||||
type Spec struct {
|
||||
Data interface{}
|
||||
Expected string
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Data: address,
|
||||
Expected: fmt.Sprintf(`"%s"`, rawAddress),
|
||||
},
|
||||
{
|
||||
Data: map[string]interface{}{
|
||||
"address": rawAddress,
|
||||
},
|
||||
Expected: fmt.Sprintf(`{"address":"%s"}`, rawAddress),
|
||||
},
|
||||
}
|
||||
|
||||
for i, spec := range specs {
|
||||
t.Run(fmt.Sprint(i+1), func(t *testing.T) {
|
||||
actual, err := json.Marshal(spec.Data)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, spec.Expected, string(actual))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressDecoding(t *testing.T) {
|
||||
rawAddress := "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh"
|
||||
address := btcutils.NewAddress(rawAddress, &chaincfg.MainNetParams)
|
||||
|
||||
// Case #1: address is a string
|
||||
t.Run("from_string", func(t *testing.T) {
|
||||
input := fmt.Sprintf(`"%s"`, rawAddress)
|
||||
expected := address
|
||||
actual := btcutils.Address{}
|
||||
|
||||
err := json.Unmarshal([]byte(input), &actual)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
// Case #2: address is a field of a struct
|
||||
t.Run("from_field_string", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Address btcutils.Address `json:"address"`
|
||||
}
|
||||
input := fmt.Sprintf(`{"address":"%s"}`, rawAddress)
|
||||
expected := Data{Address: address}
|
||||
actual := Data{}
|
||||
err := json.Unmarshal([]byte(input), &actual)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
// Case #3: address is an element of an array
|
||||
t.Run("from_array", func(t *testing.T) {
|
||||
input := fmt.Sprintf(`["%s"]`, rawAddress)
|
||||
expected := []btcutils.Address{address}
|
||||
actual := []btcutils.Address{}
|
||||
err := json.Unmarshal([]byte(input), &actual)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
// Case #4: not supported address type
|
||||
t.Run("from_string/not_address", func(t *testing.T) {
|
||||
input := fmt.Sprintf(`"%s"`, "THIS_IS_NOT_SUPPORTED_ADDRESS")
|
||||
actual := btcutils.Address{}
|
||||
err := json.Unmarshal([]byte(input), &actual)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
// Case #5: invalid field type
|
||||
t.Run("from_number", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Address btcutils.Address `json:"address"`
|
||||
}
|
||||
input := fmt.Sprintf(`{"address":%d}`, 123)
|
||||
actual := Data{}
|
||||
err := json.Unmarshal([]byte(input), &actual)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
44
pkg/btcutils/btc.go
Normal file
44
pkg/btcutils/btc.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
)
|
||||
|
||||
var (
|
||||
// NullAddress is an address that script address is all zeros.
|
||||
NullAddress = NewAddress("1111111111111111111114oLvT2", &chaincfg.MainNetParams)
|
||||
|
||||
// NullHash is a hash that all bytes are zero.
|
||||
NullHash = utils.Must(chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000"))
|
||||
)
|
||||
|
||||
// TransactionType is the type of bitcoin transaction
|
||||
// It's an alias of txscript.ScriptClass
|
||||
type TransactionType = txscript.ScriptClass
|
||||
|
||||
// AddressType is the type of bitcoin address.
|
||||
// It's an alias of txscript.ScriptClass
|
||||
type AddressType = txscript.ScriptClass
|
||||
|
||||
// Types of bitcoin transaction
|
||||
const (
|
||||
TransactionP2WPKH = txscript.WitnessV0PubKeyHashTy
|
||||
TransactionP2TR = txscript.WitnessV1TaprootTy
|
||||
TransactionTaproot = TransactionP2TR // Alias of P2TR
|
||||
TransactionP2SH = txscript.ScriptHashTy
|
||||
TransactionP2PKH = txscript.PubKeyHashTy
|
||||
TransactionP2WSH = txscript.WitnessV0ScriptHashTy
|
||||
)
|
||||
|
||||
// Types of bitcoin address
|
||||
const (
|
||||
AddressP2WPKH = txscript.WitnessV0PubKeyHashTy
|
||||
AddressP2TR = txscript.WitnessV1TaprootTy
|
||||
AddressTaproot = AddressP2TR // Alias of P2TR
|
||||
AddressP2SH = txscript.ScriptHashTy
|
||||
AddressP2PKH = txscript.PubKeyHashTy
|
||||
AddressP2WSH = txscript.WitnessV0ScriptHashTy
|
||||
)
|
||||
23
pkg/btcutils/btc_network.go
Normal file
23
pkg/btcutils/btc_network.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
)
|
||||
|
||||
var supportedNetworks = map[string]*chaincfg.Params{
|
||||
"mainnet": &chaincfg.MainNetParams,
|
||||
"testnet": &chaincfg.TestNet3Params,
|
||||
}
|
||||
|
||||
// IsSupportedNetwork returns true if the given network is supported.
|
||||
//
|
||||
// TODO: create enum for network
|
||||
func IsSupportedNetwork(network string) bool {
|
||||
_, ok := supportedNetworks[network]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetNetParams returns the *chaincfg.Params for the given network.
|
||||
func GetNetParams(network string) *chaincfg.Params {
|
||||
return supportedNetworks[network]
|
||||
}
|
||||
54
pkg/btcutils/pk_script.go
Normal file
54
pkg/btcutils/pk_script.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/cockroachdb/errors"
|
||||
)
|
||||
|
||||
// NewPkScript creates a pubkey script(or witness program) from the given address string
|
||||
//
|
||||
// see: https://en.bitcoin.it/wiki/Script
|
||||
func NewPkScript(address string, defaultNet ...*chaincfg.Params) ([]byte, error) {
|
||||
net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams)
|
||||
decoded, _, err := parseAddress(address, net)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't parse address")
|
||||
}
|
||||
scriptPubkey, err := txscript.PayToAddrScript(decoded)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't get script pubkey")
|
||||
}
|
||||
return scriptPubkey, nil
|
||||
}
|
||||
|
||||
// GetAddressTypeFromPkScript returns the address type from the given pubkey script/script pubkey.
|
||||
func GetAddressTypeFromPkScript(pkScript []byte, defaultNet ...*chaincfg.Params) (AddressType, error) {
|
||||
net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams)
|
||||
scriptClass, _, _, err := txscript.ExtractPkScriptAddrs(pkScript, net)
|
||||
if err != nil {
|
||||
return txscript.NonStandardTy, errors.Wrap(err, "can't parse pkScript")
|
||||
}
|
||||
return scriptClass, nil
|
||||
}
|
||||
|
||||
// ExtractAddressFromPkScript extracts address from the given pubkey script/script pubkey.
|
||||
// multi-signature script not supported
|
||||
func ExtractAddressFromPkScript(pkScript []byte, defaultNet ...*chaincfg.Params) (Address, error) {
|
||||
net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams)
|
||||
addrType, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, net)
|
||||
if err != nil {
|
||||
return Address{}, errors.Wrap(err, "can't parse pkScript")
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
return Address{}, errors.New("can't extract address from pkScript")
|
||||
}
|
||||
return Address{
|
||||
decoded: addrs[0],
|
||||
net: net,
|
||||
encoded: addrs[0].EncodeAddress(),
|
||||
encodedType: addrType,
|
||||
scriptPubKey: pkScript,
|
||||
}, nil
|
||||
}
|
||||
205
pkg/btcutils/pk_script_test.go
Normal file
205
pkg/btcutils/pk_script_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package btcutils_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewPkScript(t *testing.T) {
|
||||
anyError := errors.New("any error")
|
||||
|
||||
type Spec struct {
|
||||
Address string
|
||||
DefaultNet *chaincfg.Params
|
||||
ExpectedError error
|
||||
ExpectedPkScript string // hex encoded
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Address: "some_invalid_address",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: anyError,
|
||||
ExpectedPkScript: "",
|
||||
},
|
||||
{
|
||||
// P2WPKH
|
||||
Address: "bc1qdx72th7e3z8zc5wdrdxweswfcne974pjneyjln",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "001469bca5dfd9888e2c51cd1b4cecc1c9c4f25f5432",
|
||||
},
|
||||
{
|
||||
// P2WPKH
|
||||
Address: "bc1q7cj6gz6t3d28qg7kxhrc7h5t3h0re34fqqalga",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "0014f625a40b4b8b547023d635c78f5e8b8dde3cc6a9",
|
||||
},
|
||||
{
|
||||
// P2TR
|
||||
Address: "bc1pfd0zw2jwlpn4xckpr3dxpt7x0gw6wetuftxvrc4dt2qgn9azjuus65fug6",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "51204b5e272a4ef8675362c11c5a60afc67a1da7657c4accc1e2ad5a808997a29739",
|
||||
},
|
||||
{
|
||||
// P2TR
|
||||
Address: "bc1pxpumml545tqum5afarzlmnnez2npd35nvf0j0vnrp88nemqsn54qle05sm",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "51203079bdfe95a2c1cdd3a9e8c5fdce7912a616c693625f27b26309cf3cec109d2a",
|
||||
},
|
||||
{
|
||||
// P2SH
|
||||
Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "a91477e1a3d54f545d83869ae3a6b28b071422801d7b87",
|
||||
},
|
||||
{
|
||||
// P2PKH
|
||||
Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "76a914cecb25b53809991c7beef2d27bc2be49e78c684388ac",
|
||||
},
|
||||
{
|
||||
// P2WSH
|
||||
Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak",
|
||||
DefaultNet: &chaincfg.MainNetParams,
|
||||
ExpectedError: nil,
|
||||
ExpectedPkScript: "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70",
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) {
|
||||
// Validate Expected PkScript
|
||||
if spec.ExpectedError == nil {
|
||||
{
|
||||
expectedPkScriptRaw, err := hex.DecodeString(spec.ExpectedPkScript)
|
||||
if err != nil {
|
||||
t.Fatalf("can't decode expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err)
|
||||
}
|
||||
expectedPkScript, err := txscript.ParsePkScript(expectedPkScriptRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err)
|
||||
}
|
||||
|
||||
expectedAddress, err := expectedPkScript.Address(spec.DefaultNet)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get address from expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err)
|
||||
}
|
||||
assert.Equal(t, spec.Address, expectedAddress.EncodeAddress())
|
||||
}
|
||||
{
|
||||
address, err := btcutil.DecodeAddress(spec.Address, spec.DefaultNet)
|
||||
if err != nil {
|
||||
t.Fatalf("can't decode address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err)
|
||||
}
|
||||
|
||||
pkScript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get pkscript from address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err)
|
||||
}
|
||||
|
||||
pkScriptStr := hex.EncodeToString(pkScript)
|
||||
assert.Equal(t, spec.ExpectedPkScript, pkScriptStr)
|
||||
}
|
||||
}
|
||||
|
||||
pkScript, err := btcutils.NewPkScript(spec.Address, spec.DefaultNet)
|
||||
if spec.ExpectedError == anyError {
|
||||
assert.Error(t, err)
|
||||
} else if spec.ExpectedError != nil {
|
||||
assert.ErrorIs(t, err, spec.ExpectedError)
|
||||
} else {
|
||||
address, err := btcutils.SafeNewAddress(spec.Address, spec.DefaultNet)
|
||||
if err != nil {
|
||||
t.Fatalf("can't create address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err)
|
||||
}
|
||||
|
||||
// ScriptPubKey from address and from NewPkScript should be the same
|
||||
assert.Equal(t, address.ScriptPubKey(), pkScript)
|
||||
|
||||
// Expected PkScript and New PkScript should be the same
|
||||
pkScriptStr := hex.EncodeToString(pkScript)
|
||||
assert.Equal(t, spec.ExpectedPkScript, pkScriptStr)
|
||||
|
||||
// Can convert PkScript back to same address
|
||||
acualPkScript, err := txscript.ParsePkScript(address.ScriptPubKey())
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
assert.Equal(t, address.Decoded().String(), utils.Must(acualPkScript.Address(spec.DefaultNet)).String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAddressTypeFromPkScript(t *testing.T) {
|
||||
type Spec struct {
|
||||
PubkeyScript string
|
||||
|
||||
ExpectedError error
|
||||
ExpectedAddressType btcutils.AddressType
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
PubkeyScript: "0014602181cc89f7c9f54cb6d7607a3445e3e022895d",
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WPKH,
|
||||
},
|
||||
{
|
||||
PubkeyScript: "5120ef8d59038dd51093fbfff794f658a07a3697b94d9e6d24e45b28abd88f10e33d",
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2TR,
|
||||
},
|
||||
{
|
||||
PubkeyScript: "a91416eef7e84fb9821db1341b6ccef1c4a4e5ec21e487",
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2SH,
|
||||
},
|
||||
{
|
||||
PubkeyScript: "76a914cecb25b53809991c7beef2d27bc2be49e78c684388ac",
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2PKH,
|
||||
},
|
||||
{
|
||||
PubkeyScript: "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70",
|
||||
|
||||
ExpectedError: nil,
|
||||
ExpectedAddressType: btcutils.AddressP2WSH,
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("PkScript:%s", spec.PubkeyScript), func(t *testing.T) {
|
||||
pkScript, err := hex.DecodeString(spec.PubkeyScript)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
actualAddressType, actualError := btcutils.GetAddressTypeFromPkScript(pkScript)
|
||||
if spec.ExpectedError != nil {
|
||||
assert.ErrorIs(t, actualError, spec.ExpectedError)
|
||||
} else {
|
||||
assert.Equal(t, spec.ExpectedAddressType, actualAddressType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
pkg/btcutils/pkscript.go
Normal file
63
pkg/btcutils/pkscript.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ToPkScript converts a string of address or pkscript to bytes of pkscript
|
||||
func ToPkScript(network common.Network, from string) ([]byte, error) {
|
||||
if from == "" {
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "empty input")
|
||||
}
|
||||
|
||||
defaultNet, err := func() (*chaincfg.Params, error) {
|
||||
switch network {
|
||||
case common.NetworkMainnet:
|
||||
return &chaincfg.MainNetParams, nil
|
||||
case common.NetworkTestnet:
|
||||
return &chaincfg.TestNet3Params, nil
|
||||
default:
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "invalid network")
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// attempt to parse as address
|
||||
address, err := btcutil.DecodeAddress(from, defaultNet)
|
||||
if err == nil {
|
||||
pkScript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error converting address to pkscript")
|
||||
}
|
||||
return pkScript, nil
|
||||
}
|
||||
|
||||
// attempt to parse as pkscript
|
||||
pkScript, err := hex.DecodeString(from)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error decoding pkscript")
|
||||
}
|
||||
|
||||
return pkScript, nil
|
||||
}
|
||||
|
||||
// PkScriptToAddress returns the address from the given pkScript. If the pkScript is invalid or not standard, it returns empty string.
|
||||
func PkScriptToAddress(pkScript []byte, network common.Network) (string, error) {
|
||||
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, network.ChainParams())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error extracting addresses from pkscript")
|
||||
}
|
||||
if len(addrs) != 1 {
|
||||
return "", errors.New("invalid number of addresses extracted from pkscript")
|
||||
}
|
||||
return addrs[0].EncodeAddress(), nil
|
||||
}
|
||||
92
pkg/btcutils/psbtutils/encoding.go
Normal file
92
pkg/btcutils/psbtutils/encoding.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package psbtutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
)
|
||||
|
||||
const (
|
||||
// default psbt encoding is hex
|
||||
DefaultEncoding = EncodingHex
|
||||
)
|
||||
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
EncodingBase64 Encoding = "base64"
|
||||
EncodingHex Encoding = "hex"
|
||||
)
|
||||
|
||||
// DecodeString decodes a psbt hex/base64 string into a psbt.Packet
|
||||
//
|
||||
// encoding is optional, default is EncodingHex
|
||||
func DecodeString(psbtStr string, encoding ...Encoding) (*psbt.Packet, error) {
|
||||
pC, err := Decode([]byte(psbtStr), encoding...)
|
||||
return pC, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Decode decodes a psbt hex/base64 byte into a psbt.Packet
|
||||
//
|
||||
// encoding is optional, default is EncodingHex
|
||||
func Decode(psbtB []byte, encoding ...Encoding) (*psbt.Packet, error) {
|
||||
enc, ok := utils.Optional(encoding)
|
||||
if !ok {
|
||||
enc = DefaultEncoding
|
||||
}
|
||||
|
||||
var (
|
||||
psbtBytes []byte
|
||||
err error
|
||||
)
|
||||
|
||||
switch enc {
|
||||
case EncodingBase64, "b64":
|
||||
psbtBytes = make([]byte, base64.StdEncoding.DecodedLen(len(psbtB)))
|
||||
_, err = base64.StdEncoding.Decode(psbtBytes, psbtB)
|
||||
case EncodingHex:
|
||||
psbtBytes = make([]byte, hex.DecodedLen(len(psbtB)))
|
||||
_, err = hex.Decode(psbtBytes, psbtB)
|
||||
default:
|
||||
return nil, errors.Wrap(errs.Unsupported, "invalid encoding")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't decode psbt string")
|
||||
}
|
||||
|
||||
pC, err := psbt.NewFromRawBytes(bytes.NewReader(psbtBytes), false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't create psbt from given psbt")
|
||||
}
|
||||
|
||||
return pC, nil
|
||||
}
|
||||
|
||||
// EncodeToString encodes a psbt.Packet into a psbt hex/base64 string
|
||||
//
|
||||
// encoding is optional, default is EncodingHex
|
||||
func EncodeToString(pC *psbt.Packet, encoding ...Encoding) (string, error) {
|
||||
enc, ok := utils.Optional(encoding)
|
||||
if !ok {
|
||||
enc = DefaultEncoding
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pC.Serialize(&buf); err != nil {
|
||||
return "", errors.Wrap(err, "can't serialize psbt")
|
||||
}
|
||||
|
||||
switch enc {
|
||||
case EncodingBase64, "b64":
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
case EncodingHex:
|
||||
return hex.EncodeToString(buf.Bytes()), nil
|
||||
default:
|
||||
return "", errors.Wrap(errs.Unsupported, "invalid encoding")
|
||||
}
|
||||
}
|
||||
110
pkg/btcutils/psbtutils/fee.go
Normal file
110
pkg/btcutils/psbtutils/fee.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package psbtutils
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
)
|
||||
|
||||
// TxFee returns satoshis fee of a transaction given the fee rate (sat/vB)
|
||||
// and the number of inputs and outputs.
|
||||
func TxFee(feeRate int64, p *psbt.Packet) (int64, error) {
|
||||
size, err := PSBTSize(p)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "psbt size")
|
||||
}
|
||||
return int64(math.Ceil(size * float64(feeRate))), nil
|
||||
}
|
||||
|
||||
func PredictTxFee(feeRate int64, inputs, outputs int) int64 {
|
||||
/**
|
||||
TODO: handle edge cases like:
|
||||
1. when we predict that we need to use unnecessary UTXOs
|
||||
2. when we predict that we need to use more value than user have, but user do have enough for the actual transaction
|
||||
|
||||
Idea for solving this:
|
||||
- When trying to find the best UTXOs to use, we:
|
||||
- Will not reject when user's balance is not enough, instead we will return all UTXOs even if it's not enough.
|
||||
- Will be okay returning excessive UTXOs (say we predict we need 10K satoshis, but actually we only need 5K satoshis, then we will return UTXOs enough for 10K satoshis)
|
||||
- And then we:
|
||||
- Construct the actual PSBT, then select UTXOs to use accordingly,
|
||||
- If the user's balance is not enough, then we will return an error,
|
||||
- Or if when we predict we expect to use more UTXOs than the actual transaction, then we will just use what's needed.
|
||||
*/
|
||||
size := defaultOverhead + 148*float64(inputs) + 43*float64(outputs)
|
||||
return int64(math.Ceil(size * float64(feeRate)))
|
||||
}
|
||||
|
||||
type txSize struct {
|
||||
Overhead float64
|
||||
Inputs float64
|
||||
Outputs float64
|
||||
}
|
||||
|
||||
const defaultOverhead = 10.5
|
||||
|
||||
// Transaction Virtual Sizes Bytes
|
||||
//
|
||||
// Reference: https://bitcoinops.org/en/tools/calc-size/
|
||||
var txSizes = map[btcutils.TransactionType]txSize{
|
||||
btcutils.TransactionP2WPKH: {
|
||||
Inputs: 68,
|
||||
Outputs: 31,
|
||||
},
|
||||
btcutils.TransactionP2TR: {
|
||||
Inputs: 57.5,
|
||||
Outputs: 43,
|
||||
},
|
||||
btcutils.TransactionP2SH: {
|
||||
Inputs: 91,
|
||||
Outputs: 32,
|
||||
},
|
||||
btcutils.TransactionP2PKH: {
|
||||
Inputs: 148,
|
||||
Outputs: 34,
|
||||
},
|
||||
btcutils.TransactionP2WSH: {
|
||||
Inputs: 104.5,
|
||||
Outputs: 43,
|
||||
},
|
||||
}
|
||||
|
||||
func PSBTSize(psbt *psbt.Packet) (float64, error) {
|
||||
if err := psbt.SanityCheck(); err != nil {
|
||||
return 0, errors.Wrap(errors.Join(err, errs.InvalidArgument), "psbt sanity check")
|
||||
}
|
||||
|
||||
inputs := map[btcutils.TransactionType]int{}
|
||||
outputs := map[btcutils.TransactionType]int{}
|
||||
|
||||
for _, input := range psbt.Inputs {
|
||||
addrType, err := btcutils.GetAddressTypeFromPkScript(input.WitnessUtxo.PkScript)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "get address type from pk script")
|
||||
}
|
||||
inputs[addrType]++
|
||||
}
|
||||
|
||||
for _, output := range psbt.UnsignedTx.TxOut {
|
||||
addrType, err := btcutils.GetAddressTypeFromPkScript(output.PkScript)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "get address type from pk script")
|
||||
}
|
||||
outputs[addrType]++
|
||||
}
|
||||
|
||||
totalSize := defaultOverhead
|
||||
for txType, txSizeData := range txSizes {
|
||||
if inputCount, ok := inputs[txType]; ok {
|
||||
totalSize += txSizeData.Inputs * float64(inputCount)
|
||||
}
|
||||
if outputCount, ok := outputs[txType]; ok {
|
||||
totalSize += txSizeData.Outputs * float64(outputCount)
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize, nil
|
||||
}
|
||||
131
pkg/btcutils/psbtutils/fee_test.go
Normal file
131
pkg/btcutils/psbtutils/fee_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package psbtutils_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils/psbtutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPSBTSize(t *testing.T) {
|
||||
type Spec struct {
|
||||
Name string
|
||||
PSBTString string
|
||||
ExpectedError error
|
||||
ExpectedSize float64
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Name: "3-inputs-3-outputs-taproot",
|
||||
PSBTString: "70736274ff0100fd06010100000003866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910000000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910100000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910200000000ffffffff03b0040000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f22020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f4d370f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f000000000001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012bcb3c0f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f00000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedSize: 312,
|
||||
},
|
||||
{
|
||||
Name: "mixed-segwit-taproot",
|
||||
PSBTString: "70736274ff0100fd230202000000061f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90300000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90400000000ffffffff21c8ec368f2aff1a7baf4964e4070f52e7247ae39edfbda3976f8df4da1b72a00000000000ffffffff969e65b705e3d5071f1743a63381b3aa1ec31e1dbbbd63ab594a19ca399a58af0000000000ffffffffcca5cfd28bd6c54a851d97d029560b3047f7c6482fda7b2f2603d56ade8c95890000000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90500000000ffffffff0908070000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e0b03600000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa532a680000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d58020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e0b21f1e00000000001600144850d32c3ff585403790507793125d174a5c28e0000000000001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f220200000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa53010304830000000001012b22020000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d010304830000000001011f06432000000000001600144850d32c3ff585403790507793125d174a5c28e000000000000000000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedSize: 699,
|
||||
},
|
||||
{
|
||||
Name: "segwit-transfer-to-legacy",
|
||||
PSBTString: "70736274ff010074020000000124ba4becfc732f3b4729784a3dd0cc2494ae890d826377fd98aeb0607feb1ace0100000000ffffffff0210270000000000001976a91422bae94117be666b593916527d55bdaf030d756e88ac25f62e000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac000000000001011fc51d2f000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac01086b024730440220759df9d109298a1ef69b9faa1786f4118f0d4d63a68cd2061e217b6090573f62022053ffa117fc21e5bf20e7d16bb786de52dc0214c9a21af87b4e92a639ef66e997012103e0cb213a46a68b1f463a4858635ee44694ce4b512788833d629840341b1219c9000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedSize: 143.5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(spec.Name, func(t *testing.T) {
|
||||
p, err := psbtutils.DecodeString(spec.PSBTString)
|
||||
assert.NoError(t, err)
|
||||
size, err := psbtutils.PSBTSize(p)
|
||||
if spec.ExpectedError != nil {
|
||||
assert.ErrorIs(t, err, spec.ExpectedError)
|
||||
} else {
|
||||
assert.Equal(t, spec.ExpectedSize, size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPredictTxFee(t *testing.T) {
|
||||
type Spec struct {
|
||||
FeeRate int64
|
||||
InputsCount int
|
||||
OutputsCount int
|
||||
ExpectedFee int64
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
FeeRate: 100,
|
||||
InputsCount: 1,
|
||||
OutputsCount: 1,
|
||||
ExpectedFee: int64(math.Ceil((10.5 + 148 + 43) * 100)),
|
||||
},
|
||||
{
|
||||
FeeRate: 1,
|
||||
InputsCount: 99,
|
||||
OutputsCount: 99,
|
||||
ExpectedFee: int64(math.Ceil((10.5 + (99 * 148) + (99 * 43)) * 1)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(fmt.Sprintf("feeRate=%d:inputs=%d:outputs=%d", spec.FeeRate, spec.InputsCount, spec.OutputsCount), func(t *testing.T) {
|
||||
fee := psbtutils.PredictTxFee(spec.FeeRate, spec.InputsCount, spec.OutputsCount)
|
||||
assert.Equal(t, spec.ExpectedFee, fee)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxFee(t *testing.T) {
|
||||
type Spec struct {
|
||||
Name string
|
||||
FeeRate int64
|
||||
PSBTString string
|
||||
ExpectedError error
|
||||
ExpectedFee int64
|
||||
}
|
||||
|
||||
specs := []Spec{
|
||||
{
|
||||
Name: "3-inputs-3-outputs-taproot",
|
||||
FeeRate: 10,
|
||||
PSBTString: "70736274ff0100fd06010100000003866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910000000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910100000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910200000000ffffffff03b0040000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f22020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f4d370f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f000000000001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012bcb3c0f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f00000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedFee: 312 * 10,
|
||||
},
|
||||
{
|
||||
Name: "mixed-segwit-taproot",
|
||||
FeeRate: 20,
|
||||
PSBTString: "70736274ff0100fd230202000000061f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90300000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90400000000ffffffff21c8ec368f2aff1a7baf4964e4070f52e7247ae39edfbda3976f8df4da1b72a00000000000ffffffff969e65b705e3d5071f1743a63381b3aa1ec31e1dbbbd63ab594a19ca399a58af0000000000ffffffffcca5cfd28bd6c54a851d97d029560b3047f7c6482fda7b2f2603d56ade8c95890000000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90500000000ffffffff0908070000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e0b03600000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa532a680000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d58020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e0b21f1e00000000001600144850d32c3ff585403790507793125d174a5c28e0000000000001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f220200000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa53010304830000000001012b22020000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d010304830000000001011f06432000000000001600144850d32c3ff585403790507793125d174a5c28e000000000000000000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedFee: 699 * 20,
|
||||
},
|
||||
{
|
||||
Name: "segwit-transfer-to-legacy",
|
||||
FeeRate: 99,
|
||||
PSBTString: "70736274ff010074020000000124ba4becfc732f3b4729784a3dd0cc2494ae890d826377fd98aeb0607feb1ace0100000000ffffffff0210270000000000001976a91422bae94117be666b593916527d55bdaf030d756e88ac25f62e000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac000000000001011fc51d2f000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac01086b024730440220759df9d109298a1ef69b9faa1786f4118f0d4d63a68cd2061e217b6090573f62022053ffa117fc21e5bf20e7d16bb786de52dc0214c9a21af87b4e92a639ef66e997012103e0cb213a46a68b1f463a4858635ee44694ce4b512788833d629840341b1219c9000000",
|
||||
ExpectedError: nil,
|
||||
ExpectedFee: int64(math.Ceil((143.5) * 99)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, spec := range specs {
|
||||
t.Run(spec.Name, func(t *testing.T) {
|
||||
p, err := psbtutils.DecodeString(spec.PSBTString)
|
||||
assert.NoError(t, err)
|
||||
fee, err := psbtutils.TxFee(spec.FeeRate, p)
|
||||
if spec.ExpectedError != nil {
|
||||
assert.ErrorIs(t, err, spec.ExpectedError)
|
||||
} else {
|
||||
assert.Equal(t, spec.ExpectedFee, fee)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
35
pkg/btcutils/psbtutils/is_ready.go
Normal file
35
pkg/btcutils/psbtutils/is_ready.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package psbtutils
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func IsReadyPSBT(pC *psbt.Packet, feeRate int64) (bool, error) {
|
||||
// if input = output + fee then it's ready
|
||||
|
||||
// Calculate tx fee
|
||||
fee, err := TxFee(feeRate, pC)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "calculate fee")
|
||||
}
|
||||
|
||||
// sum total input and output
|
||||
totalInputValue := lo.SumBy(pC.Inputs, func(input psbt.PInput) int64 { return input.WitnessUtxo.Value })
|
||||
totalOutputValue := lo.SumBy(pC.UnsignedTx.TxOut, func(txout *wire.TxOut) int64 { return txout.Value }) + fee
|
||||
|
||||
// it's perfect match
|
||||
if totalInputValue == totalOutputValue {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if input is more than output + fee but not more than 1000 satoshi,
|
||||
// then it's ready
|
||||
if totalInputValue > totalOutputValue && totalInputValue-totalOutputValue < 1000 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
21
pkg/btcutils/signature.go
Normal file
21
pkg/btcutils/signature.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
verifier "github.com/bitonicnl/verify-signed-message/pkg"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/cockroachdb/errors"
|
||||
)
|
||||
|
||||
func VerifySignature(address string, message string, sigBase64 string, defaultNet ...*chaincfg.Params) error {
|
||||
net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams)
|
||||
_, err := verifier.VerifyWithChain(verifier.SignedMessage{
|
||||
Address: address,
|
||||
Message: message,
|
||||
Signature: sigBase64,
|
||||
}, net)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
69
pkg/btcutils/signature_test.go
Normal file
69
pkg/btcutils/signature_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package btcutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
{
|
||||
message := "Test123"
|
||||
address := "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw"
|
||||
signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8="
|
||||
network := &chaincfg.MainNetParams
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
{
|
||||
address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z"
|
||||
message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019."
|
||||
signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10="
|
||||
network := &chaincfg.TestNet3Params
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
{
|
||||
// Missmatch address
|
||||
address := "tb1qp7y2ywgrv8a4t9h47yphtgj8w759rk6vgd9ran"
|
||||
message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019."
|
||||
signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10="
|
||||
network := &chaincfg.TestNet3Params
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
{
|
||||
// Missmatch signature
|
||||
address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z"
|
||||
message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019."
|
||||
signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8="
|
||||
network := &chaincfg.TestNet3Params
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
{
|
||||
// Missmatch message
|
||||
address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z"
|
||||
message := "Hello World"
|
||||
signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10="
|
||||
network := &chaincfg.TestNet3Params
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
{
|
||||
// Missmatch network
|
||||
address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z"
|
||||
message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019."
|
||||
signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10="
|
||||
network := &chaincfg.MainNetParams
|
||||
|
||||
err := VerifySignature(address, message, signature, network)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
10
pkg/btcutils/transaction.go
Normal file
10
pkg/btcutils/transaction.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package btcutils
|
||||
|
||||
const (
|
||||
// TxVersion is the current latest supported transaction version.
|
||||
TxVersion = 2
|
||||
|
||||
// MaxTxInSequenceNum is the maximum sequence number the sequence field
|
||||
// of a transaction input can be.
|
||||
MaxTxInSequenceNum uint32 = 0xffffffff
|
||||
)
|
||||
106
pkg/decimals/decimals.go
Normal file
106
pkg/decimals/decimals.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package decimals
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/shopspring/decimal"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDivPrecision = 36
|
||||
)
|
||||
|
||||
func init() {
|
||||
decimal.DivisionPrecision = DefaultDivPrecision
|
||||
}
|
||||
|
||||
// MustFromString convert string to decimal.Decimal. Panic if error
|
||||
// string must be a valid number, not NaN, Inf or empty string.
|
||||
func MustFromString(s string) decimal.Decimal {
|
||||
return utils.Must(decimal.NewFromString(s))
|
||||
}
|
||||
|
||||
// ToDecimal convert any type to decimal.Decimal (safety floating point)
|
||||
func ToDecimal[T constraints.Integer](ivalue any, decimals T) decimal.Decimal {
|
||||
value := new(big.Int)
|
||||
switch v := ivalue.(type) {
|
||||
case string:
|
||||
value.SetString(v, 10)
|
||||
case *big.Int:
|
||||
value = v
|
||||
case int64:
|
||||
value = big.NewInt(v)
|
||||
case int, int8, int16, int32:
|
||||
rValue := reflect.ValueOf(v)
|
||||
value.SetInt64(rValue.Int())
|
||||
case uint64:
|
||||
value = big.NewInt(0).SetUint64(v)
|
||||
case uint, uint8, uint16, uint32:
|
||||
rValue := reflect.ValueOf(v)
|
||||
value.SetUint64(rValue.Uint())
|
||||
case []byte:
|
||||
value.SetBytes(v)
|
||||
case uint128.Uint128:
|
||||
value = v.Big()
|
||||
case uint256.Int:
|
||||
value = v.ToBig()
|
||||
case *uint256.Int:
|
||||
value = v.ToBig()
|
||||
}
|
||||
|
||||
switch {
|
||||
case int64(decimals) > math.MaxInt32:
|
||||
logger.Panic("ToDecimal: decimals is too big, should be equal less than 2^31-1", slogx.Any("decimals", decimals))
|
||||
case int64(decimals) < math.MinInt32+1:
|
||||
logger.Panic("ToDecimal: decimals is too small, should be greater than -2^31", slogx.Any("decimals", decimals))
|
||||
}
|
||||
|
||||
return decimal.NewFromBigInt(value, -int32(decimals))
|
||||
}
|
||||
|
||||
// ToBigInt convert any type to *big.Int
|
||||
func ToBigInt(iamount any, decimals uint16) *big.Int {
|
||||
amount := decimal.NewFromFloat(0)
|
||||
switch v := iamount.(type) {
|
||||
case string:
|
||||
amount, _ = decimal.NewFromString(v)
|
||||
case float64:
|
||||
amount = decimal.NewFromFloat(v)
|
||||
case float32:
|
||||
amount = decimal.NewFromFloat32(v)
|
||||
case int64:
|
||||
amount = decimal.NewFromInt(v)
|
||||
case int, int8, int16, int32:
|
||||
rValue := reflect.ValueOf(v)
|
||||
amount = decimal.NewFromInt(rValue.Int())
|
||||
case decimal.Decimal:
|
||||
amount = v
|
||||
case *decimal.Decimal:
|
||||
amount = *v
|
||||
case big.Float:
|
||||
amount, _ = decimal.NewFromString(v.String())
|
||||
case *big.Float:
|
||||
amount, _ = decimal.NewFromString(v.String())
|
||||
case pgtype.Numeric:
|
||||
amount = decimal.NewFromBigInt(v.Int, v.Exp)
|
||||
}
|
||||
return amount.Mul(PowerOfTen(decimals)).BigInt()
|
||||
}
|
||||
|
||||
// ToUint256 convert any type to *uint256.Int
|
||||
func ToUint256(iamount any, decimals uint16) *uint256.Int {
|
||||
result := new(uint256.Int)
|
||||
if overflow := result.SetFromBig(ToBigInt(iamount, decimals)); overflow {
|
||||
logger.Panic("ToUint256: overflow", slogx.Any("amount", iamount), slogx.Uint16("decimals", decimals))
|
||||
}
|
||||
return result
|
||||
}
|
||||
86
pkg/decimals/decimals_test.go
Normal file
86
pkg/decimals/decimals_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package decimals
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToDecimal(t *testing.T) {
|
||||
t.Run("overflow_decimals", func(t *testing.T) {
|
||||
assert.NotPanics(t, func() { ToDecimal(1, math.MaxInt32-1) }, "in-range decimals shouldn't panic")
|
||||
assert.NotPanics(t, func() { ToDecimal(1, math.MinInt32+1) }, "in-range decimals shouldn't panic")
|
||||
assert.Panics(t, func() { ToDecimal(1, math.MaxInt32+1) }, "out of range decimals should panic")
|
||||
assert.Panics(t, func() { ToDecimal(1, math.MinInt32) }, "out of range decimals should panic")
|
||||
})
|
||||
t.Run("check_supported_types", func(t *testing.T) {
|
||||
testcases := []struct {
|
||||
decimals uint16
|
||||
value uint64
|
||||
expected string
|
||||
}{
|
||||
{0, 1, "1"},
|
||||
{1, 1, "0.1"},
|
||||
{2, 1, "0.01"},
|
||||
{3, 1, "0.001"},
|
||||
{18, 1, "0.000000000000000001"},
|
||||
{36, 1, "0.000000000000000000000000000000000001"},
|
||||
}
|
||||
typesConv := []func(uint64) any{
|
||||
func(i uint64) any { return int(i) },
|
||||
func(i uint64) any { return int8(i) },
|
||||
func(i uint64) any { return int16(i) },
|
||||
func(i uint64) any { return int32(i) },
|
||||
func(i uint64) any { return int64(i) },
|
||||
func(i uint64) any { return uint(i) },
|
||||
func(i uint64) any { return uint8(i) },
|
||||
func(i uint64) any { return uint16(i) },
|
||||
func(i uint64) any { return uint32(i) },
|
||||
func(i uint64) any { return uint64(i) },
|
||||
func(i uint64) any { return fmt.Sprint(i) },
|
||||
func(i uint64) any { return new(big.Int).SetUint64(i) },
|
||||
func(i uint64) any { return new(uint128.Uint128).Add64(i) },
|
||||
func(i uint64) any { return uint256.NewInt(i) },
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%d_%d", tc.decimals, tc.value), func(t *testing.T) {
|
||||
for _, conv := range typesConv {
|
||||
input := conv(tc.value)
|
||||
t.Run(fmt.Sprintf("%T", input), func(t *testing.T) {
|
||||
actual := ToDecimal(input, tc.decimals)
|
||||
assert.Equal(t, tc.expected, actual.String())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
testcases := []struct {
|
||||
decimals uint16
|
||||
value interface{}
|
||||
expected string
|
||||
}{
|
||||
{0, uint64(math.MaxUint64), "18446744073709551615"},
|
||||
{18, uint64(math.MaxUint64), "18.446744073709551615"},
|
||||
{36, uint64(math.MaxUint64), "0.000000000000000018446744073709551615"},
|
||||
/* max uint128 */
|
||||
{0, uint128.Max, "340282366920938463463374607431768211455"},
|
||||
{18, uint128.Max, "340282366920938463463.374607431768211455"},
|
||||
{36, uint128.Max, "340.282366920938463463374607431768211455"},
|
||||
/* max uint256 */
|
||||
{0, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269984665640564039457584007913129639935"},
|
||||
{18, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269984665640564039457.584007913129639935"},
|
||||
{36, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269.984665640564039457584007913129639935"},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%d_%s", tc.decimals, tc.value), func(t *testing.T) {
|
||||
actual := ToDecimal(tc.value, tc.decimals)
|
||||
assert.Equal(t, tc.expected, actual.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
97
pkg/decimals/power_of_ten.go
Normal file
97
pkg/decimals/power_of_ten.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package decimals
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// max precision is 36
|
||||
const (
|
||||
minPowerOfTen = -DefaultDivPrecision
|
||||
maxPowerOfTen = DefaultDivPrecision
|
||||
)
|
||||
|
||||
var powerOfTen = map[int64]decimal.Decimal{
|
||||
minPowerOfTen: MustFromString("0.000000000000000000000000000000000001"),
|
||||
-35: MustFromString("0.00000000000000000000000000000000001"),
|
||||
-34: MustFromString("0.0000000000000000000000000000000001"),
|
||||
-33: MustFromString("0.000000000000000000000000000000001"),
|
||||
-32: MustFromString("0.00000000000000000000000000000001"),
|
||||
-31: MustFromString("0.0000000000000000000000000000001"),
|
||||
-30: MustFromString("0.000000000000000000000000000001"),
|
||||
-29: MustFromString("0.00000000000000000000000000001"),
|
||||
-28: MustFromString("0.0000000000000000000000000001"),
|
||||
-27: MustFromString("0.000000000000000000000000001"),
|
||||
-26: MustFromString("0.00000000000000000000000001"),
|
||||
-25: MustFromString("0.0000000000000000000000001"),
|
||||
-24: MustFromString("0.000000000000000000000001"),
|
||||
-23: MustFromString("0.00000000000000000000001"),
|
||||
-22: MustFromString("0.0000000000000000000001"),
|
||||
-21: MustFromString("0.000000000000000000001"),
|
||||
-20: MustFromString("0.00000000000000000001"),
|
||||
-19: MustFromString("0.0000000000000000001"),
|
||||
-18: MustFromString("0.000000000000000001"),
|
||||
-17: MustFromString("0.00000000000000001"),
|
||||
-16: MustFromString("0.0000000000000001"),
|
||||
-15: MustFromString("0.000000000000001"),
|
||||
-14: MustFromString("0.00000000000001"),
|
||||
-13: MustFromString("0.0000000000001"),
|
||||
-12: MustFromString("0.000000000001"),
|
||||
-11: MustFromString("0.00000000001"),
|
||||
-10: MustFromString("0.0000000001"),
|
||||
-9: MustFromString("0.000000001"),
|
||||
-8: MustFromString("0.00000001"),
|
||||
-7: MustFromString("0.0000001"),
|
||||
-6: MustFromString("0.000001"),
|
||||
-5: MustFromString("0.00001"),
|
||||
-4: MustFromString("0.0001"),
|
||||
-3: MustFromString("0.001"),
|
||||
-2: MustFromString("0.01"),
|
||||
-1: MustFromString("0.1"),
|
||||
0: MustFromString("1"),
|
||||
1: MustFromString("10"),
|
||||
2: MustFromString("100"),
|
||||
3: MustFromString("1000"),
|
||||
4: MustFromString("10000"),
|
||||
5: MustFromString("100000"),
|
||||
6: MustFromString("1000000"),
|
||||
7: MustFromString("10000000"),
|
||||
8: MustFromString("100000000"),
|
||||
9: MustFromString("1000000000"),
|
||||
10: MustFromString("10000000000"),
|
||||
11: MustFromString("100000000000"),
|
||||
12: MustFromString("1000000000000"),
|
||||
13: MustFromString("10000000000000"),
|
||||
14: MustFromString("100000000000000"),
|
||||
15: MustFromString("1000000000000000"),
|
||||
16: MustFromString("10000000000000000"),
|
||||
17: MustFromString("100000000000000000"),
|
||||
18: MustFromString("1000000000000000000"),
|
||||
19: MustFromString("10000000000000000000"),
|
||||
20: MustFromString("100000000000000000000"),
|
||||
21: MustFromString("1000000000000000000000"),
|
||||
22: MustFromString("10000000000000000000000"),
|
||||
23: MustFromString("100000000000000000000000"),
|
||||
24: MustFromString("1000000000000000000000000"),
|
||||
25: MustFromString("10000000000000000000000000"),
|
||||
26: MustFromString("100000000000000000000000000"),
|
||||
27: MustFromString("1000000000000000000000000000"),
|
||||
28: MustFromString("10000000000000000000000000000"),
|
||||
29: MustFromString("100000000000000000000000000000"),
|
||||
30: MustFromString("1000000000000000000000000000000"),
|
||||
31: MustFromString("10000000000000000000000000000000"),
|
||||
32: MustFromString("100000000000000000000000000000000"),
|
||||
33: MustFromString("1000000000000000000000000000000000"),
|
||||
34: MustFromString("10000000000000000000000000000000000"),
|
||||
35: MustFromString("100000000000000000000000000000000000"),
|
||||
maxPowerOfTen: MustFromString("1000000000000000000000000000000000000"),
|
||||
}
|
||||
|
||||
// PowerOfTen optimized arithmetic performance for 10^n.
|
||||
func PowerOfTen[T constraints.Integer](n T) decimal.Decimal {
|
||||
nInt64 := int64(n)
|
||||
if val, ok := powerOfTen[nInt64]; ok {
|
||||
return val
|
||||
}
|
||||
return powerOfTen[1].Pow(decimal.NewFromInt(nInt64))
|
||||
}
|
||||
44
pkg/decimals/power_of_ten_test.go
Normal file
44
pkg/decimals/power_of_ten_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package decimals
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPowerOfTen(t *testing.T) {
|
||||
for n := int64(-36); n <= 36; n++ {
|
||||
t.Run(fmt.Sprint(n), func(t *testing.T) {
|
||||
expected := powerOfTenString(n)
|
||||
actual := PowerOfTen(n)
|
||||
assert.Equal(t, expected, actual.String())
|
||||
})
|
||||
}
|
||||
t.Run("constants", func(t *testing.T) {
|
||||
for n, p := range powerOfTen {
|
||||
t.Run(p.String(), func(t *testing.T) {
|
||||
require.False(t, p.IsZero(), "power of ten must not be zero")
|
||||
actual := PowerOfTen(n)
|
||||
assert.Equal(t, p, actual)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// powerOfTenString add zero padding to power of ten string
|
||||
func powerOfTenString(n int64) string {
|
||||
s := "1"
|
||||
if n < 0 {
|
||||
for i := int64(0); i < -n-1; i++ {
|
||||
s = "0" + s
|
||||
}
|
||||
s = "0." + s
|
||||
} else {
|
||||
for i := int64(0); i < n; i++ {
|
||||
s = s + "0"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
7
pkg/middleware/requestcontext/PROXY-IP.md
Normal file
7
pkg/middleware/requestcontext/PROXY-IP.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Proxies IP Range Resources
|
||||
|
||||
- Cloudflare - https://www.cloudflare.com/ips/
|
||||
- GCP Load Balancer - https://cloud.google.com/load-balancing/docs/health-check-concepts#ip-ranges
|
||||
- GCP Compute Engine, Customer-usable external IP address ranges - https://www.gstatic.com/ipranges/cloud.json
|
||||
- Other GCP Services - https://cloud.google.com/compute/docs/faq#networking
|
||||
- Other Resources - https://github.com/lord-alfred/ipranges
|
||||
21
pkg/middleware/requestcontext/errors.go
Normal file
21
pkg/middleware/requestcontext/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package requestcontext
|
||||
|
||||
// requestcontextError implements error interface
|
||||
var _ error = requestcontextError{}
|
||||
|
||||
type requestcontextError struct {
|
||||
err error
|
||||
status int
|
||||
message string
|
||||
}
|
||||
|
||||
func (r requestcontextError) Error() string {
|
||||
if r.err != nil {
|
||||
return r.err.Error()
|
||||
}
|
||||
return r.message
|
||||
}
|
||||
|
||||
func (r requestcontextError) Unwrap() error {
|
||||
return r.err
|
||||
}
|
||||
44
pkg/middleware/requestcontext/requestcontext.go
Normal file
44
pkg/middleware/requestcontext/requestcontext.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package requestcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Result any `json:"result"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Option func(ctx context.Context, c *fiber.Ctx) (context.Context, error)
|
||||
|
||||
func New(opts ...Option) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
var err error
|
||||
ctx := c.UserContext()
|
||||
for i, opt := range opts {
|
||||
ctx, err = opt(ctx, c)
|
||||
if err != nil {
|
||||
rErr := requestcontextError{}
|
||||
if errors.As(err, &rErr) {
|
||||
return c.Status(rErr.status).JSON(Response{Error: rErr.message})
|
||||
}
|
||||
|
||||
logger.ErrorContext(ctx, "failed to extract request context",
|
||||
err,
|
||||
slog.String("event", "requestcontext/error"),
|
||||
slog.String("module", "requestcontext"),
|
||||
slog.Int("optionIndex", i),
|
||||
)
|
||||
return c.Status(http.StatusInternalServerError).JSON(Response{Error: "internal server error"})
|
||||
}
|
||||
}
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
150
pkg/middleware/requestcontext/with_clientip.go
Normal file
150
pkg/middleware/requestcontext/with_clientip.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package requestcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type clientIPKey struct{}
|
||||
|
||||
type WithClientIPConfig struct {
|
||||
// [Optional] TrustedProxiesIP is a list of all proxies IP ranges that's between the server and the client.
|
||||
//
|
||||
// If it's provided, it will walk backwards from the last IP in `X-Forwarded-For` header
|
||||
// and use first IP that's not trusted proxy(not in the given IP ranges.)
|
||||
//
|
||||
// **If you want to use this option, you should provide all of probable proxies IP ranges.**
|
||||
//
|
||||
// This is lowest priority.
|
||||
TrustedProxiesIP []string `env:"TRUSTED_PROXIES_IP" mapstructure:"trusted_proxies_ip"`
|
||||
|
||||
// [Optional] TrustedHeader is a header name for getting client IP. (e.g. X-Real-IP, CF-Connecting-IP, etc.)
|
||||
//
|
||||
// This is highest priority, it will ignore rest of the options if it's provided.
|
||||
TrustedHeader string `env:"TRUSTED_HEADER" mapstructure:"trusted_proxies_header"`
|
||||
|
||||
// EnableRejectMalformedRequest return 403 Forbidden if the request is from proxies, but can't extract client IP
|
||||
EnableRejectMalformedRequest bool `env:"ENABLE_REJECT_MALFORMED_REQUEST" envDefault:"false" mapstructure:"enable_reject_malformed_request"`
|
||||
}
|
||||
|
||||
// WithClientIP setup client IP context with XFF Spoofing prevention support.
|
||||
//
|
||||
// If request is from proxies, it will use first IP from `X-Forwarded-For` header by default.
|
||||
func WithClientIP(config WithClientIPConfig) Option {
|
||||
var trustedProxies trustedProxy
|
||||
if len(config.TrustedProxiesIP) > 0 {
|
||||
proxy, err := newTrustedProxy(config.TrustedProxiesIP)
|
||||
if err != nil {
|
||||
logger.Panic("Failed to parse trusted proxies", err)
|
||||
}
|
||||
trustedProxies = proxy
|
||||
}
|
||||
|
||||
return func(ctx context.Context, c *fiber.Ctx) (context.Context, error) {
|
||||
// Extract client IP from given header
|
||||
if config.TrustedHeader != "" {
|
||||
headerIP := c.Get(config.TrustedHeader)
|
||||
|
||||
// validate ip from header
|
||||
if ip := net.ParseIP(headerIP); ip != nil {
|
||||
return context.WithValue(ctx, clientIPKey{}, headerIP), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Extract client IP from XFF header
|
||||
rawIPs := c.IPs()
|
||||
ips := parseIPs(rawIPs)
|
||||
|
||||
// If the request is directly from client, we can use direct remote IP address
|
||||
if len(ips) == 0 {
|
||||
return context.WithValue(ctx, clientIPKey{}, c.IP()), nil
|
||||
}
|
||||
|
||||
// Walk back and find first IP that's not trusted proxy
|
||||
if len(trustedProxies) > 0 {
|
||||
for i := len(ips) - 1; i >= 0; i-- {
|
||||
if !trustedProxies.IsTrusted(ips[i]) {
|
||||
return context.WithValue(ctx, clientIPKey{}, ips[i].String()), nil
|
||||
}
|
||||
}
|
||||
|
||||
// If all IPs are trusted proxies, return first IP in XFF header
|
||||
return context.WithValue(ctx, clientIPKey{}, rawIPs[0]), nil
|
||||
}
|
||||
|
||||
// Finally, if we can't extract client IP, return forbidden
|
||||
if config.EnableRejectMalformedRequest {
|
||||
logger.WarnContext(ctx, "IP Spoofing detected, returning 403 Forbidden",
|
||||
slog.String("event", "requestcontext/ip_spoofing_detected"),
|
||||
slog.String("module", "requestcontext/with_clientip"),
|
||||
slog.String("ip", c.IP()),
|
||||
slog.Any("ips", rawIPs),
|
||||
)
|
||||
return nil, requestcontextError{
|
||||
status: fiber.StatusForbidden,
|
||||
message: "not allowed to access",
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first IP in XFF header
|
||||
return context.WithValue(ctx, clientIPKey{}, rawIPs[0]), nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientIP get clientIP from context. If not found, return empty string
|
||||
//
|
||||
// Warning: Request context should be setup before using this function
|
||||
func GetClientIP(ctx context.Context) string {
|
||||
if ip, ok := ctx.Value(clientIPKey{}).(string); ok {
|
||||
return ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type trustedProxy []*net.IPNet
|
||||
|
||||
// newTrustedProxy create a new trusted proxies instance for preventing IP spoofing (XFF Attacks)
|
||||
func newTrustedProxy(ranges []string) (trustedProxy, error) {
|
||||
nets, err := parseCIDRs(ranges)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return trustedProxy(nets), nil
|
||||
}
|
||||
|
||||
func (t trustedProxy) IsTrusted(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, r := range t {
|
||||
if r.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseCIDRs(ranges []string) ([]*net.IPNet, error) {
|
||||
nets := make([]*net.IPNet, 0, len(ranges))
|
||||
for _, r := range ranges {
|
||||
_, ipnet, err := net.ParseCIDR(r)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse CIDR for %q", r)
|
||||
}
|
||||
nets = append(nets, ipnet)
|
||||
}
|
||||
return nets, nil
|
||||
}
|
||||
|
||||
func parseIPs(ranges []string) []net.IP {
|
||||
ip := make([]net.IP, 0, len(ranges))
|
||||
for _, r := range ranges {
|
||||
ip = append(ip, net.ParseIP(r))
|
||||
}
|
||||
return ip
|
||||
}
|
||||
47
pkg/middleware/requestcontext/with_requestid.go
Normal file
47
pkg/middleware/requestcontext/with_requestid.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package requestcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
fiberutils "github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
type requestIdKey struct{}
|
||||
|
||||
// GetRequestId get requestId from context. If not found, return empty string
|
||||
//
|
||||
// Warning: Request context should be setup before using this function
|
||||
func GetRequestId(ctx context.Context) string {
|
||||
if id, ok := ctx.Value(requestIdKey{}).(string); ok {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func WithRequestId() Option {
|
||||
return func(ctx context.Context, c *fiber.Ctx) (context.Context, error) {
|
||||
// Try to get id from fiber context.
|
||||
requestId, ok := c.Locals(requestid.ConfigDefault.ContextKey).(string)
|
||||
if !ok || requestId == "" {
|
||||
// Try to get id from request, else we generate one
|
||||
requestId = c.Get(requestid.ConfigDefault.Header, fiberutils.UUID())
|
||||
|
||||
// Set new id to response header
|
||||
c.Set(requestid.ConfigDefault.Header, requestId)
|
||||
|
||||
// Add the request ID to locals (fasthttp UserValue storage)
|
||||
c.Locals(requestid.ConfigDefault.ContextKey, requestId)
|
||||
}
|
||||
|
||||
// Add the request ID to context
|
||||
ctx = context.WithValue(ctx, requestIdKey{}, requestId)
|
||||
|
||||
// Add the requuest ID to context logger
|
||||
ctx = logger.WithContext(ctx, "requestId", requestId)
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
}
|
||||
115
pkg/middleware/requestlogger/requestlogger.go
Normal file
115
pkg/middleware/requestlogger/requestlogger.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package requestlogger
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
WithRequestHeader bool `env:"REQUEST_HEADER" envDefault:"false" mapstructure:"request_header"`
|
||||
WithRequestQuery bool `env:"REQUEST_QUERY" envDefault:"false" mapstructure:"request_query"`
|
||||
Disable bool `env:"DISABLE" envDefault:"false" mapstructure:"disable"` // Disable logger level `INFO`
|
||||
HiddenRequestHeaders []string `env:"HIDDEN_REQUEST_HEADERS" mapstructure:"hidden_request_headers"`
|
||||
}
|
||||
|
||||
// New setup request context and information
|
||||
func New(config Config) fiber.Handler {
|
||||
hiddenRequestHeaders := make(map[string]struct{}, len(config.HiddenRequestHeaders))
|
||||
for _, header := range config.HiddenRequestHeaders {
|
||||
hiddenRequestHeaders[strings.TrimSpace(strings.ToLower(header))] = struct{}{}
|
||||
}
|
||||
return func(c *fiber.Ctx) error {
|
||||
start := time.Now()
|
||||
|
||||
// Continue stack
|
||||
err := c.Next()
|
||||
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
status := c.Response().StatusCode()
|
||||
|
||||
baseAttrs := []slog.Attr{
|
||||
slog.String("event", "api_request"),
|
||||
slog.Int64("latency", latency.Milliseconds()),
|
||||
slog.String("latencyHuman", latency.String()),
|
||||
}
|
||||
|
||||
// prep request attributes
|
||||
requestAttributes := []slog.Attr{
|
||||
slog.Time("time", start),
|
||||
slog.String("method", c.Method()),
|
||||
slog.String("host", c.Hostname()),
|
||||
slog.String("path", c.Path()),
|
||||
slog.String("route", c.Route().Path),
|
||||
slog.String("ip", requestcontext.GetClientIP(c.UserContext())),
|
||||
slog.String("remoteIP", c.Context().RemoteIP().String()),
|
||||
slog.Any("x-forwarded-for", c.IPs()),
|
||||
slog.String("user-agent", string(c.Context().UserAgent())),
|
||||
slog.Any("params", c.AllParams()),
|
||||
slog.Int("length", len((c.Body()))),
|
||||
}
|
||||
|
||||
// prep response attributes
|
||||
responseAttributes := []slog.Attr{
|
||||
slog.Time("time", end),
|
||||
slog.Int("status", status),
|
||||
slog.Int("length", len(c.Response().Body())),
|
||||
}
|
||||
|
||||
// request query
|
||||
if config.WithRequestQuery {
|
||||
requestAttributes = append(requestAttributes, slog.String("query", string(c.Request().URI().QueryString())))
|
||||
}
|
||||
|
||||
// request headers
|
||||
if config.WithRequestHeader {
|
||||
kv := []any{}
|
||||
|
||||
for k, v := range c.GetReqHeaders() {
|
||||
if _, found := hiddenRequestHeaders[strings.ToLower(k)]; found {
|
||||
continue
|
||||
}
|
||||
kv = append(kv, slog.Any(k, v))
|
||||
}
|
||||
|
||||
requestAttributes = append(requestAttributes, slog.Group("header", kv...))
|
||||
}
|
||||
|
||||
level := slog.LevelInfo
|
||||
if err != nil || status >= http.StatusInternalServerError {
|
||||
level = slog.LevelError
|
||||
|
||||
// error attributes
|
||||
logErr := err
|
||||
if logErr == nil {
|
||||
logErr = fiber.NewError(status)
|
||||
}
|
||||
baseAttrs = append(baseAttrs, slog.Any("error", logErr))
|
||||
}
|
||||
|
||||
if config.Disable && level == slog.LevelInfo {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.LogAttrs(c.UserContext(), level, "Request Completed", append([]slog.Attr{
|
||||
{
|
||||
Key: "request",
|
||||
Value: slog.GroupValue(requestAttributes...),
|
||||
},
|
||||
{
|
||||
Key: "response",
|
||||
Value: slog.GroupValue(responseAttributes...),
|
||||
},
|
||||
}, baseAttrs...)...,
|
||||
)
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user