Compare commits

..

14 Commits

Author SHA1 Message Date
Waris Aiemworawutikul
83cb5a9cfa Merge branch 'develop' into nodesale 2024-06-13 16:00:35 +07:00
Waris Aiemworawutikul
9e11dd3c67 feat: update nodesale protocol to include sellerWallet 2024-06-11 15:04:30 +07:00
Waris Aiemworawutikul
9b5caa589c fix: make it UTC time just in case 2024-06-11 13:16:18 +07:00
Waris Aiemworawutikul
0a77e3ff0f feat: Implemented nodesale indexer API 2024-06-11 13:14:28 +07:00
Waris Aiemworawutikul
0ddcc8ea46 fix: simplify txPubkey extraction 2024-06-07 17:43:26 +07:00
Waris Aiemworawutikul
30bc624624 feat: cleanup and prepare for implementing nodesale indexer API 2024-06-07 14:53:10 +07:00
Waris Aiemworawutikul
6672556383 fix: fix bug delegate 2024-06-06 12:56:45 +07:00
Waris Aiemworawutikul
73ac0ef6b5 feat: prepare unit tests 2024-06-05 18:23:15 +07:00
Waris Aiemworawutikul
2223bcf1d0 fix: merge with development error. 2024-06-04 15:14:02 +07:00
Waris Aiemworawutikul
a75c87d09a Merge branch 'develop' into nodesale 2024-06-04 15:09:46 +07:00
Waris Aiemworawutikul
d563ddbed2 feat: mostly implemented nodesale protocol parser. 2024-06-04 14:57:08 +07:00
Waris Aiemworawutikul
80db77de6a feat: implement parsing deploy message and delegate message. 2024-05-31 15:31:48 +07:00
Waris Aiemworawutikul
62ec809af6 feat: add protobuf nodesales 2024-05-29 16:07:51 +07:00
Waris Aiemworawutikul
1aa358d504 feat: create module nodesale 2024-05-28 15:49:52 +07:00
96 changed files with 1847 additions and 6419 deletions

View File

@@ -58,9 +58,6 @@ jobs:
cache: true # caching and restoring go modules and build outputs.
- run: echo "GOVERSION=$(go version)" >> $GITHUB_ENV
- name: Touch test result file
run: echo "" > test_output.json
- name: Build
run: go build -v ./...

View File

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

View File

@@ -2,7 +2,7 @@
# Gaze Indexer
Gaze Indexer is an open-source and modular indexing client for Bitcoin meta-protocols with **Unified Consistent APIs** across fungible token protocols.
Gaze Indexer is an open-source and modular indexing client for Bitcoin meta-protocols. It has support for Runes out of the box, with **Unified Consistent APIs** across fungible token protocols.
Gaze Indexer is built with **modularity** in mind, allowing users to run all modules in one monolithic instance with a single command, or as a distributed cluster of micro-services.

View File

@@ -20,9 +20,9 @@ import (
"github.com/gaze-network/indexer-network/modules/nodesale"
"github.com/gaze-network/indexer-network/modules/runes"
"github.com/gaze-network/indexer-network/pkg/automaxprocs"
"github.com/gaze-network/indexer-network/pkg/errorhandler"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/middleware/errorhandler"
"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"
@@ -138,16 +138,8 @@ func runHandler(cmd *cobra.Command, _ []string) error {
// Initialize HTTP server
do.Provide(injector, func(i do.Injector) (*fiber.App, error) {
app := fiber.New(fiber.Config{
AppName: "Gaze Indexer",
ErrorHandler: func(c *fiber.Ctx, err error) error {
logger.ErrorContext(c.UserContext(), "Something went wrong, unhandled api error",
slogx.String("event", "api_unhandled_error"),
slogx.Error(err),
)
return errors.WithStack(c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal Server Error",
}))
},
AppName: "Gaze Indexer",
ErrorHandler: errorhandler.NewHTTPErrorHandler(),
})
app.
Use(favicon.New()).
@@ -166,7 +158,6 @@ func runHandler(cmd *cobra.Command, _ []string) error {
logger.ErrorContext(c.UserContext(), "Something went wrong, panic in http handler", slogx.Any("panic", e), slog.String("stacktrace", string(buf)))
},
})).
Use(errorhandler.New()).
Use(compress.New(compress.Config{
Level: compress.LevelDefault,
}))

View File

@@ -52,6 +52,5 @@ modules:
host: "localhost"
port: 5432
user: "postgres"
password: "P@ssw0rd"
password: "password"
db_name: "postgres"
last_block_default: 400

View File

@@ -243,32 +243,39 @@ func (d *BitcoinNodeDatasource) prepareRange(fromHeight, toHeight int64) (start,
}
// GetTransaction fetch transaction from Bitcoin node
func (d *BitcoinNodeDatasource) GetRawTransactionAndHeightByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, int64, error) {
func (d *BitcoinNodeDatasource) GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error) {
rawTxVerbose, err := d.btcclient.GetRawTransactionVerbose(&txHash)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get raw transaction")
return nil, errors.Wrap(err, "failed to get raw transaction")
}
blockHash, err := chainhash.NewHashFromStr(rawTxVerbose.BlockHash)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to parse block hash")
return nil, errors.Wrap(err, "failed to parse block hash")
}
block, err := d.btcclient.GetBlockVerbose(blockHash)
block, err := d.btcclient.GetBlockVerboseTx(blockHash)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get block header")
return nil, errors.Wrap(err, "failed to get block header")
}
// parse tx
txBytes, err := hex.DecodeString(rawTxVerbose.Hex)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to decode transaction hex")
return nil, errors.Wrap(err, "failed to decode transaction hex")
}
var msgTx wire.MsgTx
if err := msgTx.Deserialize(bytes.NewReader(txBytes)); err != nil {
return nil, 0, errors.Wrap(err, "failed to deserialize transaction")
return nil, errors.Wrap(err, "failed to deserialize transaction")
}
var txIndex uint32
for i, tx := range block.Tx {
if tx.Hex == rawTxVerbose.Hex {
txIndex = uint32(i)
break
}
}
return &msgTx, block.Height, nil
return types.ParseMsgTx(&msgTx, block.Height, *blockHash, txIndex), nil
}
// GetBlockHeader fetch block header from Bitcoin node
@@ -285,12 +292,3 @@ func (d *BitcoinNodeDatasource) GetBlockHeader(ctx context.Context, height int64
return types.ParseMsgBlockHeader(*block, height), nil
}
func (d *BitcoinNodeDatasource) GetRawTransactionByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, error) {
transaction, err := d.btcclient.GetRawTransaction(&txHash)
if err != nil {
return nil, errors.Wrap(err, "failed to get raw transaction")
}
return transaction.MsgTx(), nil
}

22
go.mod
View File

@@ -5,10 +5,11 @@ go 1.22
require (
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11
github.com/btcsuite/btcd v0.24.0
github.com/btcsuite/btcd/btcec/v2 v2.1.3
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/cockroachdb/errors v1.11.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/gaze-network/uint128 v1.3.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-migrate/migrate/v4 v4.17.1
@@ -21,27 +22,22 @@ 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.9.0
github.com/stretchr/testify v1.8.4
github.com/valyala/fasthttp v1.51.0
go.uber.org/automaxprocs v1.5.3
golang.org/x/sync v0.7.0
golang.org/x/sync v0.5.0
google.golang.org/protobuf v1.33.0
)
require github.com/stretchr/objx v0.5.2 // indirect
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bitonicnl/verify-signed-message v0.7.1
github.com/btcsuite/btcd/btcec/v2 v2.3.3
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.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/decred/dcrd/crypto/blake256 v1.0.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
@@ -79,10 +75,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.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
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
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

50
go.sum
View File

@@ -7,23 +7,18 @@ 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=
@@ -55,12 +50,10 @@ 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/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 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
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=
@@ -103,8 +96,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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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=
@@ -221,15 +214,12 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
@@ -253,14 +243,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.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/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/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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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=
@@ -275,8 +265,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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/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=
@@ -289,19 +279,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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/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.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
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=

View File

@@ -63,7 +63,7 @@ type BitcoinNodeClient struct {
type Modules struct {
Runes runesconfig.Config `mapstructure:"runes"`
NodeSale nodesaleconfig.Config `mapstructure:"nodesale"`
Nodesale nodesaleconfig.Config `mapstructure:"nodesale"`
}
type HTTPServerConfig struct {

View File

@@ -1,99 +0,0 @@
package httphandler
import (
"fmt"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gofiber/fiber/v2"
"google.golang.org/protobuf/encoding/protojson"
)
type deployRequest struct {
DeployID string `params:"deployId"`
}
type tierResponse struct {
PriceSat uint32 `json:"priceSat"`
Limit uint32 `json:"limit"`
MaxPerAddress uint32 `json:"maxPerAddress"`
Sold int64 `json:"sold"`
}
type deployResponse struct {
Id string `json:"id"`
Name string `json:"name"`
StartsAt int64 `json:"startsAt"`
EndsAt int64 `json:"endsAt"`
Tiers []tierResponse `json:"tiers"`
SellerPublicKey string `json:"sellerPublicKey"`
MaxPerAddress uint32 `json:"maxPerAddress"`
DeployTxHash string `json:"deployTxHash"`
}
func (h *handler) deployHandler(ctx *fiber.Ctx) error {
var request deployRequest
err := ctx.ParamsParser(&request)
if err != nil {
return errors.Wrap(err, "cannot parse param")
}
var blockHeight uint64
var txIndex uint32
count, err := fmt.Sscanf(request.DeployID, "%d-%d", &blockHeight, &txIndex)
if count != 2 || err != nil {
return errs.NewPublicError("Invalid deploy ID")
}
deploys, err := h.nodeSaleDg.GetNodeSale(ctx.UserContext(), datagateway.GetNodeSaleParams{
BlockHeight: blockHeight,
TxIndex: txIndex,
})
if err != nil {
return errors.Wrap(err, "Cannot get NodeSale from db")
}
if len(deploys) < 1 {
return errs.NewPublicError("NodeSale not found")
}
deploy := deploys[0]
nodeCount, err := h.nodeSaleDg.GetNodeCountByTierIndex(ctx.UserContext(), datagateway.GetNodeCountByTierIndexParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
FromTier: 0,
ToTier: uint32(len(deploy.Tiers) - 1),
})
if err != nil {
return errors.Wrap(err, "Cannot get node count from db")
}
tiers := make([]protobuf.Tier, len(deploy.Tiers))
tierResponses := make([]tierResponse, len(deploy.Tiers))
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
return errors.Wrap(err, "Failed to decode tiers json")
}
tierResponses[i].Limit = tiers[i].Limit
tierResponses[i].MaxPerAddress = tiers[i].MaxPerAddress
tierResponses[i].PriceSat = tiers[i].PriceSat
tierResponses[i].Sold = nodeCount[i].Count
}
err = ctx.JSON(&deployResponse{
Id: request.DeployID,
Name: deploy.Name,
StartsAt: deploy.StartsAt.UTC().Unix(),
EndsAt: deploy.EndsAt.UTC().Unix(),
Tiers: tierResponses,
SellerPublicKey: deploy.SellerPublicKey,
MaxPerAddress: deploy.MaxPerAddress,
DeployTxHash: deploy.DeployTxHash,
})
if err != nil {
return errors.Wrap(err, "Go fiber cannot parse JSON")
}
return nil
}

View File

@@ -1,56 +0,0 @@
package httphandler
import (
"encoding/json"
"time"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/gofiber/fiber/v2"
)
type eventRequest struct {
WalletAddress string `query:"walletAddress"`
}
type eventResposne struct {
TxHash string `json:"txHash"`
BlockHeight int64 `json:"blockHeight"`
TxIndex int32 `json:"txIndex"`
WalletAddress string `json:"walletAddress"`
Action string `json:"action"`
ParsedMessage json.RawMessage `json:"parsedMessage"`
BlockTimestamp time.Time `json:"blockTimestamp"`
BlockHash string `json:"blockHash"`
}
func (h *handler) eventsHandler(ctx *fiber.Ctx) error {
var request eventRequest
err := ctx.QueryParser(&request)
if err != nil {
return errors.Wrap(err, "cannot parse query")
}
events, err := h.nodeSaleDg.GetEventsByWallet(ctx.UserContext(), request.WalletAddress)
if err != nil {
return errors.Wrap(err, "Can't get events from db")
}
responses := make([]eventResposne, len(events))
for i, event := range events {
responses[i].TxHash = event.TxHash
responses[i].BlockHeight = event.BlockHeight
responses[i].TxIndex = event.TxIndex
responses[i].WalletAddress = event.WalletAddress
responses[i].Action = protobuf.Action_name[event.Action]
responses[i].ParsedMessage = event.ParsedMessage
responses[i].BlockTimestamp = event.BlockTimestamp
responses[i].BlockHash = event.BlockHash
}
err = ctx.JSON(responses)
if err != nil {
return errors.Wrap(err, "Go fiber cannot parse JSON")
}
return nil
}

View File

@@ -1,15 +0,0 @@
package httphandler
import (
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
)
type handler struct {
nodeSaleDg datagateway.NodeSaleDataGateway
}
func New(datagateway datagateway.NodeSaleDataGateway) *handler {
h := handler{}
h.nodeSaleDg = datagateway
return &h
}

View File

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

View File

@@ -1,26 +0,0 @@
package httphandler
import (
"github.com/cockroachdb/errors"
"github.com/gofiber/fiber/v2"
)
type infoResponse struct {
IndexedBlockHeight int64 `json:"indexedBlockHeight"`
IndexedBlockHash string `json:"indexedBlockHash"`
}
func (h *handler) infoHandler(ctx *fiber.Ctx) error {
block, err := h.nodeSaleDg.GetLastProcessedBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "Cannot get last processed block")
}
err = ctx.JSON(infoResponse{
IndexedBlockHeight: block.BlockHeight,
IndexedBlockHash: block.BlockHash,
})
if err != nil {
return errors.Wrap(err, "Go fiber cannot parse JSON")
}
return nil
}

View File

@@ -1,82 +0,0 @@
package httphandler
import (
"fmt"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gofiber/fiber/v2"
)
type nodeRequest struct {
DeployId string `query:"deployId"`
OwnerPublicKey string `query:"ownerPublicKey"`
DelegateePublicKey string `query:"delegateePublicKey"`
}
type nodeResponse struct {
DeployId string `json:"deployId"`
NodeId uint32 `json:"nodeId"`
TierIndex int32 `json:"tierIndex"`
DelegatedTo string `json:"delegatedTo"`
OwnerPublicKey string `json:"ownerPublicKey"`
PurchaseTxHash string `json:"purchaseTxHash"`
DelegateTxHash string `json:"delegateTxHash"`
PurchaseBlockHeight int32 `json:"purchaseBlockHeight"`
}
func (h *handler) nodesHandler(ctx *fiber.Ctx) error {
var request nodeRequest
err := ctx.QueryParser(&request)
if err != nil {
return errors.Wrap(err, "cannot parse query")
}
ownerPublicKey := request.OwnerPublicKey
delegateePublicKey := request.DelegateePublicKey
var blockHeight int64
var txIndex int32
count, err := fmt.Sscanf(request.DeployId, "%d-%d", &blockHeight, &txIndex)
if count != 2 || err != nil {
return errs.NewPublicError("Invalid deploy ID")
}
var nodes []entity.Node
if ownerPublicKey == "" {
nodes, err = h.nodeSaleDg.GetNodesByDeployment(ctx.UserContext(), blockHeight, txIndex)
if err != nil {
return errors.Wrap(err, "Can't get nodes from db")
}
} else {
nodes, err = h.nodeSaleDg.GetNodesByPubkey(ctx.UserContext(), datagateway.GetNodesByPubkeyParams{
SaleBlock: blockHeight,
SaleTxIndex: txIndex,
OwnerPublicKey: ownerPublicKey,
DelegatedTo: delegateePublicKey,
})
if err != nil {
return errors.Wrap(err, "Can't get nodes from db")
}
}
responses := make([]nodeResponse, len(nodes))
for i, node := range nodes {
responses[i].DeployId = request.DeployId
responses[i].NodeId = node.NodeID
responses[i].TierIndex = node.TierIndex
responses[i].DelegatedTo = node.DelegatedTo
responses[i].OwnerPublicKey = node.OwnerPublicKey
responses[i].PurchaseTxHash = node.PurchaseTxHash
responses[i].DelegateTxHash = node.DelegateTxHash
responses[i].PurchaseBlockHeight = txIndex
}
err = ctx.JSON(responses)
if err != nil {
return errors.Wrap(err, "Go fiber cannot parse JSON")
}
return nil
}

View File

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

View File

@@ -3,6 +3,5 @@ package config
import "github.com/gaze-network/indexer-network/internal/postgres"
type Config struct {
Postgres postgres.Config `mapstructure:"postgres"`
LastBlockDefault int64 `mapstructure:"last_block_default"`
Postgres postgres.Config `mapstructure:"postgres"`
}

View File

@@ -1,7 +1,7 @@
BEGIN;
CREATE TABLE IF NOT EXISTS blocks (
"block_height" BIGINT NOT NULL,
"block_height" INTEGER NOT NULL,
"block_hash" TEXT NOT NULL,
"module" TEXT NOT NULL,
PRIMARY KEY("block_height", "block_hash")
@@ -9,17 +9,16 @@ CREATE TABLE IF NOT EXISTS blocks (
CREATE TABLE IF NOT EXISTS events (
"tx_hash" TEXT NOT NULL PRIMARY KEY,
"block_height" BIGINT NOT NULL,
"block_height" INTEGER NOT NULL,
"tx_index" INTEGER NOT NULL,
"wallet_address" TEXT NOT NULL,
"valid" BOOLEAN NOT NULL,
"action" INTEGER NOT NULL,
"raw_message" BYTEA NOT NULL,
"parsed_message" JSONB NOT NULL DEFAULT '{}',
"parsed_message" JSONB NOT NULL,
"block_timestamp" TIMESTAMP NOT NULL,
"block_hash" TEXT NOT NULL,
"metadata" JSONB NOT NULL DEFAULT '{}',
"reason" TEXT NOT NULL DEFAULT ''
"metadata" JSONB NOT NULL
);
INSERT INTO events("tx_hash", "block_height", "tx_index",
@@ -32,7 +31,7 @@ VALUES ('', -1, -1,
'', '{}');
CREATE TABLE IF NOT EXISTS node_sales (
"block_height" BIGINT NOT NULL,
"block_height" INTEGER NOT NULL,
"tx_index" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"starts_at" TIMESTAMP NOT NULL,
@@ -47,7 +46,7 @@ CREATE TABLE IF NOT EXISTS node_sales (
);
CREATE TABLE IF NOT EXISTS nodes (
"sale_block" BIGINT NOT NULL,
"sale_block" INTEGER NOT NULL,
"sale_tx_index" INTEGER NOT NULL,
"node_id" INTEGER NOT NULL,
"tier_index" INTEGER NOT NULL,

View File

@@ -1,5 +1,6 @@
-- name: GetLastProcessedBlock :one
SELECT * FROM blocks ORDER BY block_height DESC LIMIT 1;
SELECT * FROM blocks
WHERE "block_height" = (SELECT MAX("block_height") FROM blocks);
-- name: GetBlock :one
@@ -10,6 +11,6 @@ WHERE "block_height" = $1;
DELETE FROM blocks
WHERE "block_height" >= @from_block;
-- name: CreateBlock :exec
INSERT INTO blocks ("block_height", "block_hash", "module")
-- name: AddBlock :exec
INSERT INTO blocks("block_height", "block_hash", "module")
VALUES ($1, $2, $3);

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +0,0 @@
package datagateway
import (
"context"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
)
type NodeSaleDataGateway interface {
BeginNodeSaleTx(ctx context.Context) (NodeSaleDataGatewayWithTx, error)
CreateBlock(ctx context.Context, arg entity.Block) error
GetBlock(ctx context.Context, blockHeight int64) (*entity.Block, error)
GetLastProcessedBlock(ctx context.Context) (*entity.Block, error)
RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error)
RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error)
ClearDelegate(ctx context.Context) (int64, error)
GetNodesByIds(ctx context.Context, arg GetNodesByIdsParams) ([]entity.Node, error)
CreateEvent(ctx context.Context, arg entity.NodeSaleEvent) error
SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error)
CreateNodeSale(ctx context.Context, arg entity.NodeSale) error
GetNodeSale(ctx context.Context, arg GetNodeSaleParams) ([]entity.NodeSale, error)
GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams) ([]entity.Node, error)
CreateNode(ctx context.Context, arg entity.Node) error
GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error)
GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]entity.Node, error)
GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error)
GetEventsByWallet(ctx context.Context, walletAddress string) ([]entity.NodeSaleEvent, error)
}
type NodeSaleDataGatewayWithTx interface {
NodeSaleDataGateway
Tx
}
type GetNodesByIdsParams struct {
SaleBlock uint64
SaleTxIndex uint32
NodeIds []uint32
}
type SetDelegatesParams struct {
SaleBlock uint64
SaleTxIndex int32
Delegatee string
DelegateTxHash string
NodeIds []uint32
}
type GetNodeSaleParams struct {
BlockHeight uint64
TxIndex uint32
}
type GetNodesByOwnerParams struct {
SaleBlock uint64
SaleTxIndex uint32
OwnerPublicKey string
}
type GetNodeCountByTierIndexParams struct {
SaleBlock uint64
SaleTxIndex uint32
FromTier uint32
ToTier uint32
}
type GetNodeCountByTierIndexRow struct {
TierIndex int32
Count int64
}
type GetNodesByPubkeyParams struct {
SaleBlock int64
SaleTxIndex int32
OwnerPublicKey string
DelegatedTo string
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,21 @@
package nodesale
import (
"context"
"encoding/hex"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway/mocks"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/samber/lo"
"github.com/stretchr/testify/mock"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
)
func TestDeployInvalid(t *testing.T) {
ctx := context.Background()
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
prvKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
strangerKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
strangerPubkeyHex := hex.EncodeToString(strangerKey.PubKey().SerializeCompressed())
sellerWallet := p.PubkeyToPkHashAddress(prvKey.PubKey())
if testing.Short() {
t.Skip("skipping test in short mode.")
}
prvKey, _ := btcec.NewPrivateKey()
sellerWallet := p.pubkeyToPkHashAddress(prvKey.PubKey())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
@@ -50,7 +34,7 @@ func TestDeployInvalid(t *testing.T) {
MaxPerAddress: 100,
},
},
SellerPublicKey: strangerPubkeyHex,
SellerPublicKey: "0102030405",
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),
@@ -58,38 +42,28 @@ func TestDeployInvalid(t *testing.T) {
}
event, block := assembleTestEvent(prvKey, "0101010101", "0101010101", 0, 0, message)
p.processDeploy(ctx, qtx, block, event)
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
return event.Valid == false
})).Return(nil)
err = p.ProcessDeploy(ctx, mockDgTx, block, event)
require.NoError(t, err)
mockDgTx.AssertNotCalled(t, "CreateNodeSale")
nodesales, _ := qtx.GetNodesale(ctx, gen.GetNodesaleParams{
BlockHeight: int32(testBlockHeigh) - 1,
TxIndex: int32(testTxIndex) - 1,
})
require.Len(t, nodesales, 0)
}
func TestDeployValid(t *testing.T) {
ctx := context.Background()
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
privateKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
privateKey, _ := btcec.NewPrivateKey()
pubkeyHex := hex.EncodeToString(privateKey.PubKey().SerializeCompressed())
sellerWallet := p.PubkeyToPkHashAddress(privateKey.PubKey())
startAt := time.Now().Add(time.Hour * -1)
endAt := time.Now().Add(time.Hour * 1)
sellerWallet := p.pubkeyToPkHashAddress(privateKey.PubKey())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
Deploy: &protobuf.ActionDeploy{
Name: t.Name(),
StartsAt: uint32(startAt.UTC().Unix()),
EndsAt: uint32(endAt.UTC().Unix()),
StartsAt: 100,
EndsAt: 200,
Tiers: []*protobuf.Tier{
{
PriceSat: 100,
@@ -110,30 +84,11 @@ func TestDeployValid(t *testing.T) {
}
event, block := assembleTestEvent(privateKey, "0202020202", "0202020202", 0, 0, message)
p.processDeploy(ctx, qtx, block, event)
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
return event.Valid == true
})).Return(nil)
tiers := lo.Map(message.Deploy.Tiers, func(tier *protobuf.Tier, _ int) []byte {
tierJson, err := protojson.Marshal(tier)
require.NoError(t, err)
return tierJson
nodesales, _ := qtx.GetNodesale(ctx, gen.GetNodesaleParams{
BlockHeight: int32(testBlockHeigh) - 1,
TxIndex: int32(testTxIndex) - 1,
})
mockDgTx.EXPECT().CreateNodeSale(mock.Anything, entity.NodeSale{
BlockHeight: uint64(event.Transaction.BlockHeight),
TxIndex: uint32(event.Transaction.Index),
Name: message.Deploy.Name,
StartsAt: time.Unix(int64(message.Deploy.StartsAt), 0),
EndsAt: time.Unix(int64(message.Deploy.EndsAt), 0),
Tiers: tiers,
SellerPublicKey: message.Deploy.SellerPublicKey,
MaxPerAddress: message.Deploy.MaxPerAddress,
DeployTxHash: event.Transaction.TxHash.String(),
MaxDiscountPercentage: int32(message.Deploy.MaxDiscountPercentage),
SellerWallet: message.Deploy.SellerWallet,
}).Return(nil)
p.ProcessDeploy(ctx, mockDgTx, block, event)
require.Len(t, nodesales, 1)
}

View File

@@ -1,55 +0,0 @@
package entity
import "time"
type Block struct {
BlockHeight int64
BlockHash string
Module string
}
type Node struct {
SaleBlock uint64
SaleTxIndex uint32
NodeID uint32
TierIndex int32
DelegatedTo string
OwnerPublicKey string
PurchaseTxHash string
DelegateTxHash string
}
type NodeSale struct {
BlockHeight uint64
TxIndex uint32
Name string
StartsAt time.Time
EndsAt time.Time
Tiers [][]byte
SellerPublicKey string
MaxPerAddress uint32
DeployTxHash string
MaxDiscountPercentage int32
SellerWallet string
}
type NodeSaleEvent struct {
TxHash string
BlockHeight int64
TxIndex int32
WalletAddress string
Valid bool
Action int32
RawMessage []byte
ParsedMessage []byte
BlockTimestamp time.Time
BlockHash string
Metadata *MetadataEventPurchase
Reason string
}
type MetadataEventPurchase struct {
ExpectedTotalAmountDiscounted uint64
ReportedTotalAmount uint64
PaidTotalAmount uint64
}

View File

@@ -1,51 +0,0 @@
package delegate
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
)
type DelegateValidator struct {
validator.Validator
}
func New() *DelegateValidator {
v := validator.New()
return &DelegateValidator{
Validator: *v,
}
}
func (v *DelegateValidator) NodesExist(
ctx context.Context,
qtx datagateway.NodeSaleDataGatewayWithTx,
deployId *protobuf.ActionID,
nodeIds []uint32,
) (bool, []entity.Node, error) {
if !v.Valid {
return false, nil, nil
}
nodes, err := qtx.GetNodesByIds(ctx, datagateway.GetNodesByIdsParams{
SaleBlock: deployId.Block,
SaleTxIndex: deployId.TxIndex,
NodeIds: nodeIds,
})
if err != nil {
v.Valid = false
return v.Valid, nil, errors.Wrap(err, "Failed to get nodes")
}
if len(nodeIds) != len(nodes) {
v.Valid = false
return v.Valid, nil, nil
}
v.Valid = true
return v.Valid, nodes, nil
}

View File

@@ -1,6 +0,0 @@
package validator
const (
INVALID_PUBKEY_FORMAT = "Cannot parse public key"
INVALID_PUBKEY = "Invalid public key"
)

View File

@@ -1,17 +0,0 @@
package purchase
const (
DEPLOYID_NOT_FOUND = "Depoloy ID not found."
PURCHASE_TIMEOUT = "Purchase timeout."
BLOCK_HEIGHT_TIMEOUT = "Block height over timeout block"
INVALID_SIGNATURE_FORMAT = "Cannot parse signature."
INVALID_SIGNATURE = "Invalid Signature."
INVALID_TIER_JSON = "Invalid Tier format"
INVALID_NODE_ID = "Invalid NodeId."
NODE_ALREADY_PURCHASED = "Some node has been purchased."
INVALID_SELLER_ADDR_FORMAT = "Invalid seller address."
INVALID_PAYMENT = "Total amount paid less than reported price"
INSUFFICIENT_FUND = "Insufficient fund"
OVER_LIMIT_PER_ADDR = "Purchase over limit per address."
OVER_LIMIT_PER_TIER = "Purchase over limit per tier."
)

View File

@@ -1,283 +0,0 @@
package purchase
import (
"context"
"encoding/hex"
"slices"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type PurchaseValidator struct {
validator.Validator
}
func New() *PurchaseValidator {
v := validator.New()
return &PurchaseValidator{
Validator: *v,
}
}
func (v *PurchaseValidator) NodeSaleExists(ctx context.Context, qtx datagateway.NodeSaleDataGatewayWithTx, payload *protobuf.PurchasePayload) (bool, *entity.NodeSale, error) {
if !v.Valid {
return false, nil, nil
}
// check node existed
deploys, err := qtx.GetNodeSale(ctx, datagateway.GetNodeSaleParams{
BlockHeight: payload.DeployID.Block,
TxIndex: payload.DeployID.TxIndex,
})
if err != nil {
v.Valid = false
return v.Valid, nil, errors.Wrap(err, "Failed to Get NodeSale")
}
if len(deploys) < 1 {
v.Valid = false
v.Reason = DEPLOYID_NOT_FOUND
return v.Valid, nil, nil
}
v.Valid = true
return v.Valid, &deploys[0], nil
}
func (v *PurchaseValidator) ValidTimestamp(deploy *entity.NodeSale, timestamp time.Time) bool {
if !v.Valid {
return false
}
if timestamp.Before(deploy.StartsAt) ||
timestamp.After(deploy.EndsAt) {
v.Valid = false
v.Reason = PURCHASE_TIMEOUT
return v.Valid
}
v.Valid = true
return v.Valid
}
func (v *PurchaseValidator) WithinTimeoutBlock(timeOutBlock uint64, blockHeight uint64) bool {
if !v.Valid {
return false
}
if timeOutBlock == 0 {
// No timeout
v.Valid = true
return v.Valid
}
if timeOutBlock < blockHeight {
v.Valid = false
v.Reason = BLOCK_HEIGHT_TIMEOUT
return v.Valid
}
v.Valid = true
return v.Valid
}
func (v *PurchaseValidator) VerifySignature(purchase *protobuf.ActionPurchase, deploy *entity.NodeSale) bool {
if !v.Valid {
return false
}
payload := purchase.Payload
payloadBytes, _ := proto.Marshal(payload)
signatureBytes, _ := hex.DecodeString(purchase.SellerSignature)
signature, err := ecdsa.ParseSignature(signatureBytes)
if err != nil {
v.Valid = false
v.Reason = INVALID_SIGNATURE_FORMAT
return v.Valid
}
hash := chainhash.DoubleHashB(payloadBytes)
pubkeyBytes, _ := hex.DecodeString(deploy.SellerPublicKey)
pubKey, _ := btcec.ParsePubKey(pubkeyBytes)
verified := signature.Verify(hash[:], pubKey)
if !verified {
v.Valid = false
v.Reason = INVALID_SIGNATURE
return v.Valid
}
v.Valid = true
return v.Valid
}
type TierMap struct {
Tiers []protobuf.Tier
BuyingTiersCount []uint32
NodeIdToTier map[uint32]int32
}
func (v *PurchaseValidator) ValidTiers(
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
) (bool, TierMap) {
if !v.Valid {
return false, TierMap{}
}
tiers := make([]protobuf.Tier, len(deploy.Tiers))
buyingTiersCount := make([]uint32, len(tiers))
nodeIdToTier := make(map[uint32]int32)
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
v.Valid = false
v.Reason = INVALID_TIER_JSON
return v.Valid, TierMap{}
}
}
slices.Sort(payload.NodeIDs)
var currentTier int32 = -1
var tierSum uint32 = 0
for _, nodeId := range payload.NodeIDs {
for nodeId >= tierSum && currentTier < int32(len(tiers)-1) {
currentTier++
tierSum += tiers[currentTier].Limit
}
if nodeId < tierSum {
buyingTiersCount[currentTier]++
nodeIdToTier[nodeId] = currentTier
} else {
v.Valid = false
v.Reason = INVALID_NODE_ID
return false, TierMap{}
}
}
v.Valid = true
return v.Valid, TierMap{
Tiers: tiers,
BuyingTiersCount: buyingTiersCount,
NodeIdToTier: nodeIdToTier,
}
}
func (v *PurchaseValidator) ValidUnpurchasedNodes(
ctx context.Context,
qtx datagateway.NodeSaleDataGatewayWithTx,
payload *protobuf.PurchasePayload,
) (bool, error) {
if !v.Valid {
return false, nil
}
// valid unpurchased node ID
nodes, err := qtx.GetNodesByIds(ctx, datagateway.GetNodesByIdsParams{
SaleBlock: payload.DeployID.Block,
SaleTxIndex: payload.DeployID.TxIndex,
NodeIds: payload.NodeIDs,
})
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "Failed to Get nodes")
}
if len(nodes) > 0 {
v.Valid = false
v.Reason = NODE_ALREADY_PURCHASED
return false, nil
}
v.Valid = true
return true, nil
}
func (v *PurchaseValidator) ValidPaidAmount(
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
txPaid uint64,
tiers []protobuf.Tier,
buyingTiersCount []uint32,
network *chaincfg.Params,
) (bool, *entity.MetadataEventPurchase) {
if !v.Valid {
return false, nil
}
meta := entity.MetadataEventPurchase{}
meta.PaidTotalAmount = txPaid
meta.ReportedTotalAmount = uint64(payload.TotalAmountSat)
// total amount paid is greater than report paid
if txPaid < uint64(payload.TotalAmountSat) {
v.Valid = false
v.Reason = INVALID_PAYMENT
return v.Valid, nil
}
// calculate total price
var totalPrice uint64 = 0
for i := 0; i < len(tiers); i++ {
totalPrice += uint64(buyingTiersCount[i] * tiers[i].PriceSat)
}
// report paid is greater than max discounted total price
maxDiscounted := totalPrice * (100 - uint64(deploy.MaxDiscountPercentage))
decimal := maxDiscounted % 100
maxDiscounted /= 100
if decimal%100 >= 50 {
maxDiscounted++
}
meta.ExpectedTotalAmountDiscounted = maxDiscounted
if uint64(payload.TotalAmountSat) < maxDiscounted {
v.Valid = false
v.Reason = INSUFFICIENT_FUND
return v.Valid, nil
}
v.Valid = true
return v.Valid, &meta
}
func (v *PurchaseValidator) WithinLimit(
ctx context.Context,
qtx datagateway.NodeSaleDataGatewayWithTx,
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
tiers []protobuf.Tier,
buyingTiersCount []uint32,
) (bool, error) {
if !v.Valid {
return false, nil
}
// check node limit
// get all selled by seller and owned by buyer
buyerOwnedNodes, err := qtx.GetNodesByOwner(ctx, datagateway.GetNodesByOwnerParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
OwnerPublicKey: payload.BuyerPublicKey,
})
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "Failed to GetNodesByOwner")
}
if len(buyerOwnedNodes)+len(payload.NodeIDs) > int(deploy.MaxPerAddress) {
v.Valid = false
v.Reason = "Purchase over limit per address."
return v.Valid, nil
}
// check limit
// count each tiers
// check limited for each tier
ownedTiersCount := make([]uint32, len(tiers))
for _, node := range buyerOwnedNodes {
ownedTiersCount[node.TierIndex]++
}
for i := 0; i < len(tiers); i++ {
if ownedTiersCount[i]+buyingTiersCount[i] > tiers[i].MaxPerAddress {
v.Valid = false
v.Reason = "Purchase over limit per tier."
return v.Valid, nil
}
}
v.Valid = true
return v.Valid, nil
}

View File

@@ -1,44 +0,0 @@
package validator
import (
"bytes"
"encoding/hex"
"github.com/btcsuite/btcd/btcec/v2"
)
type Validator struct {
Valid bool
Reason string
}
func New() *Validator {
return &Validator{
Valid: true,
}
}
func (v *Validator) EqualXonlyPublicKey(target string, expected *btcec.PublicKey) bool {
if !v.Valid {
return false
}
targetBytes, err := hex.DecodeString(target)
if err != nil {
v.Valid = false
v.Reason = INVALID_PUBKEY_FORMAT
}
targetPubKey, err := btcec.ParsePubKey(targetBytes)
if err != nil {
v.Valid = false
v.Reason = INVALID_PUBKEY_FORMAT
}
xOnlyTargetPubKey := btcec.ToSerialized(targetPubKey).SchnorrSerialized()
xOnlyExpectedPubKey := btcec.ToSerialized(expected).SchnorrSerialized()
v.Valid = bytes.Equal(xOnlyTargetPubKey[:], xOnlyExpectedPubKey[:])
if !v.Valid {
v.Reason = INVALID_PUBKEY
}
return v.Valid
}

View File

@@ -16,10 +16,11 @@ import (
"github.com/samber/do/v2"
)
var NODESALE_MAGIC = []byte{0x6e, 0x73, 0x6f, 0x70}
var NODESALE_MAGIC = []byte{0x63, 0x73, 0x6f, 0x70}
const (
Version = "v0.0.1-alpha"
NODESALE_LASTBLOCK_DEFAULT = 846851
Version = "v0.0.1-alpha"
)
func New(injector do.Injector) (indexer.IndexerWorker, error) {
@@ -29,7 +30,7 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
btcClient := do.MustInvoke[*rpcclient.Client](injector)
datasource := datasources.NewBitcoinNode(btcClient)
pg, err := postgres.NewPool(ctx, conf.Modules.NodeSale.Postgres)
pg, err := postgres.NewPool(ctx, conf.Modules.Nodesale.Postgres)
if err != nil {
return nil, fmt.Errorf("Can't create postgres connection : %w", err)
}
@@ -41,21 +42,20 @@ func New(injector do.Injector) (indexer.IndexerWorker, error) {
repository := repository.NewRepository(pg)
processor := &Processor{
NodeSaleDg: repository,
BtcClient: datasource,
Network: conf.Network,
cleanupFuncs: cleanupFuncs,
lastBlockDefault: conf.Modules.NodeSale.LastBlockDefault,
repository: repository,
btcClient: datasource,
network: conf.Network,
cleanupFuncs: cleanupFuncs,
}
httpServer := do.MustInvoke[*fiber.App](injector)
nodeSaleHandler := httphandler.New(repository)
if err := nodeSaleHandler.Mount(httpServer); err != nil {
nodesaleHandler := httphandler.New(repository)
if err := nodesaleHandler.Mount(httpServer); err != nil {
return nil, fmt.Errorf("Can't mount nodesale API : %w", err)
}
logger.InfoContext(ctx, "Mounted nodesale HTTP handler")
indexer := indexer.New(processor, datasource)
logger.InfoContext(ctx, "NodeSale module started.")
logger.InfoContext(ctx, "Nodesale module started.")
return indexer, nil
}

View File

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

View File

@@ -3,6 +3,8 @@ package nodesale
import (
"bytes"
"context"
"fmt"
"log/slog"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -11,72 +13,35 @@ import (
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
)
type NodeSaleEvent struct {
Transaction *types.Transaction
EventMessage *protobuf.NodeSaleEvent
EventJson []byte
TxPubkey *btcec.PublicKey
RawData []byte
InputValue uint64
}
func NewProcessor(repository datagateway.NodeSaleDataGateway,
datasource *datasources.BitcoinNodeDatasource,
network common.Network,
cleanupFuncs []func(context.Context) error,
lastBlockDefault int64,
) *Processor {
return &Processor{
NodeSaleDg: repository,
BtcClient: datasource,
Network: network,
cleanupFuncs: cleanupFuncs,
lastBlockDefault: lastBlockDefault,
}
}
func (p *Processor) Shutdown(ctx context.Context) error {
for _, cleanupFunc := range p.cleanupFuncs {
err := cleanupFunc(ctx)
if err != nil {
return errors.Wrap(err, "cleanup function error")
}
}
return nil
}
type Processor struct {
NodeSaleDg datagateway.NodeSaleDataGateway
BtcClient *datasources.BitcoinNodeDatasource
Network common.Network
cleanupFuncs []func(context.Context) error
lastBlockDefault int64
repository *repository.Repository
btcClient *datasources.BitcoinNodeDatasource
network common.Network
cleanupFuncs []func(context.Context) error
}
// CurrentBlock implements indexer.Processor.
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
block, err := p.NodeSaleDg.GetLastProcessedBlock(ctx)
block, err := p.repository.Queries.GetLastProcessedBlock(ctx)
if err != nil {
logger.InfoContext(ctx, "Couldn't get last processed block. Start from NODESALE_LAST_BLOCK_DEFAULT.",
slogx.Int64("currentBlock", p.lastBlockDefault))
header, err := p.BtcClient.GetBlockHeader(ctx, p.lastBlockDefault)
slog.Int("currentBlock", NODESALE_LASTBLOCK_DEFAULT))
header, err := p.btcClient.GetBlockHeader(ctx, NODESALE_LASTBLOCK_DEFAULT)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "Cannot get default block from bitcoin node")
return types.BlockHeader{}, fmt.Errorf("Cannot get default block from bitcoin node : %w", err)
}
return types.BlockHeader{
Hash: header.Hash,
Height: p.lastBlockDefault,
Height: NODESALE_LASTBLOCK_DEFAULT,
}, nil
}
@@ -86,15 +51,15 @@ func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error)
}
return types.BlockHeader{
Hash: *hash,
Height: block.BlockHeight,
Height: int64(block.BlockHeight),
}, nil
}
// GetIndexedBlock implements indexer.Processor.
func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error) {
block, err := p.NodeSaleDg.GetBlock(ctx, height)
block, err := p.repository.Queries.GetBlock(ctx, int32(height))
if err != nil {
return types.BlockHeader{}, errors.Wrapf(err, "Block %d not found", height)
return types.BlockHeader{}, fmt.Errorf("Block %d not found : %w", height, err)
}
hash, err := chainhash.NewHashFromStr(block.BlockHash)
if err != nil {
@@ -102,7 +67,7 @@ func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.Bl
}
return types.BlockHeader{
Hash: *hash,
Height: block.BlockHeight,
Height: int64(block.BlockHeight),
}, nil
}
@@ -111,7 +76,7 @@ func (p *Processor) Name() string {
return "nodesale"
}
func extractNodeSaleData(witness [][]byte) (data []byte, internalPubkey *btcec.PublicKey, isNodeSale bool) {
func extractNodesaleData(witness [][]byte) (data []byte, internalPubkey *btcec.PublicKey, isNodesale bool) {
tokenizer, controlBlock, isTapScript := extractTapScript(witness)
if !isTapScript {
return []byte{}, nil, false
@@ -139,8 +104,7 @@ func extractNodeSaleData(witness [][]byte) (data []byte, internalPubkey *btcec.P
state = 0
}
case 3:
// Any instruction > txscript.OP_16 is not push data. Note: txscript.OP_PUSHDATAX < txscript.OP_16
if tokenizer.Opcode() <= txscript.OP_16 {
if tokenizer.Opcode() == txscript.OP_PUSHDATA1 {
data := tokenizer.Data()
return data, controlBlock.InternalKey, true
}
@@ -150,12 +114,22 @@ func extractNodeSaleData(witness [][]byte) (data []byte, internalPubkey *btcec.P
return []byte{}, nil, false
}
func (p *Processor) parseTransactions(ctx context.Context, transactions []*types.Transaction) ([]NodeSaleEvent, error) {
var events []NodeSaleEvent
type nodesaleEvent struct {
transaction *types.Transaction
eventMessage *protobuf.NodeSaleEvent
eventJson []byte
// txAddress btcutil.Address
txPubkey *btcec.PublicKey
rawData []byte
// rawScript []byte
}
func (p *Processor) parseTransactions(ctx context.Context, transactions []*types.Transaction) ([]nodesaleEvent, error) {
var events []nodesaleEvent
for _, t := range transactions {
for _, txIn := range t.TxIn {
data, txPubkey, isNodeSale := extractNodeSaleData(txIn.Witness)
if !isNodeSale {
data, txPubkey, isNodesale := extractNodesaleData(txIn.Witness)
if !isNodesale {
continue
}
@@ -163,31 +137,39 @@ func (p *Processor) parseTransactions(ctx context.Context, transactions []*types
err := proto.Unmarshal(data, event)
if err != nil {
logger.WarnContext(ctx, "Invalid Protobuf",
slogx.String("block_hash", t.BlockHash.String()),
slogx.Int("txIndex", int(t.Index)))
slog.String("block_hash", t.BlockHash.String()),
slog.Int("txIndex", int(t.Index)))
continue
}
eventJson, err := protojson.Marshal(event)
if err != nil {
return []NodeSaleEvent{}, errors.Wrap(err, "Failed to parse protobuf to json")
return []nodesaleEvent{}, fmt.Errorf("Failed to parse protobuf to json : %w", err)
}
prevTx, _, err := p.BtcClient.GetRawTransactionAndHeightByTxHash(ctx, txIn.PreviousOutTxHash)
if err != nil {
return nil, errors.Wrap(err, "Failed to get Previous transaction data")
}
/*
outIndex := txIn.PreviousOutIndex
outHash := txIn.PreviousOutTxHash
result, err := p.btcClient.GetTransactionByHash(ctx, outHash)
if err != nil {
return []nodesaleEvent{}, fmt.Errorf("Failed to Get Bitcoin transaction : %w", err)
}
pkScript := result.TxOut[outIndex].PkScript
_, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, p.network.ChainParams())
if err != nil {
return []nodesaleEvent{}, fmt.Errorf("Failed to Get Bitcoin address : %w", err)
}
if len(addresses) != 1 {
return []nodesaleEvent{}, fmt.Errorf("Multiple addresses detected.")
}*/
if txIn.PreviousOutIndex >= uint32(len(prevTx.TxOut)) {
return nil, errors.Wrap(err, "Invalid previous transaction from bitcoin")
}
events = append(events, NodeSaleEvent{
Transaction: t,
EventMessage: event,
EventJson: eventJson,
RawData: data,
TxPubkey: txPubkey,
InputValue: uint64(prevTx.TxOut[txIn.PreviousOutIndex].Value),
events = append(events, nodesaleEvent{
transaction: t,
eventMessage: event,
eventJson: eventJson,
// txAddress: addresses[0],
rawData: data,
txPubkey: txPubkey,
// rawScript: rawScript,
})
}
}
@@ -197,101 +179,96 @@ func (p *Processor) parseTransactions(ctx context.Context, transactions []*types
// Process implements indexer.Processor.
func (p *Processor) Process(ctx context.Context, inputs []*types.Block) error {
for _, block := range inputs {
logger.InfoContext(ctx, "NodeSale processing a block",
slogx.Int64("block", block.Header.Height),
slogx.Stringer("hash", block.Header.Hash))
logger.InfoContext(ctx, "Nodesale processing a block",
slog.Int64("block", block.Header.Height),
slog.String("hash", block.Header.Hash.String()))
// parse all event from each transaction including reading tx wallet
events, err := p.parseTransactions(ctx, block.Transactions)
if err != nil {
return errors.Wrap(err, "Invalid data from bitcoin client")
return fmt.Errorf("Invalid data from bitcoin client : %w", err)
}
// open transaction
qtx, err := p.NodeSaleDg.BeginNodeSaleTx(ctx)
tx, err := p.repository.Db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create transaction")
return fmt.Errorf("Failed to create transaction : %w", err)
}
defer func() {
err = qtx.Rollback(ctx)
if err != nil {
logger.PanicContext(ctx, "Failed to rollback db")
}
}()
defer tx.Rollback(ctx)
qtx := p.repository.WithTx(tx)
// write block
err = qtx.CreateBlock(ctx, entity.Block{
BlockHeight: block.Header.Height,
err = qtx.AddBlock(ctx, gen.AddBlockParams{
BlockHeight: int32(block.Header.Height),
BlockHash: block.Header.Hash.String(),
Module: p.Name(),
})
if err != nil {
return errors.Wrapf(err, "Failed to add block %d", block.Header.Height)
return fmt.Errorf("Failed to add block %d : %w", block.Header.Height, err)
}
// for each events
for _, event := range events {
logger.InfoContext(ctx, "NodeSale processing event",
slogx.Uint32("txIndex", event.Transaction.Index),
slogx.Int64("blockHeight", block.Header.Height),
slogx.Stringer("blockhash", block.Header.Hash),
logger.InfoContext(ctx, "Nodesale processing event",
slog.Int("txIndex", int(event.transaction.Index)),
slog.Int("blockHeight", int(block.Header.Height)),
slog.String("blockhash", block.Header.Hash.String()),
)
eventMessage := event.EventMessage
eventMessage := event.eventMessage
switch eventMessage.Action {
case protobuf.Action_ACTION_DEPLOY:
err = p.ProcessDeploy(ctx, qtx, block, event)
err = p.processDeploy(ctx, qtx, block, event)
if err != nil {
return errors.Wrapf(err, "Failed to deploy at block %d", block.Header.Height)
return fmt.Errorf("Failed to deploy at block %d : %w", block.Header.Height, err)
}
case protobuf.Action_ACTION_DELEGATE:
err = p.ProcessDelegate(ctx, qtx, block, event)
err = p.processDelegate(ctx, qtx, block, event)
if err != nil {
return errors.Wrapf(err, "Failed to delegate at block %d", block.Header.Height)
return fmt.Errorf("Failed to delegate at block %d : %w", block.Header.Height, err)
}
case protobuf.Action_ACTION_PURCHASE:
err = p.ProcessPurchase(ctx, qtx, block, event)
err = p.processPurchase(ctx, qtx, block, event)
if err != nil {
return errors.Wrapf(err, "Failed to purchase at block %d", block.Header.Height)
return fmt.Errorf("Failed to purchase at block %d : %w", block.Header.Height, err)
}
default:
logger.DebugContext(ctx, "Invalid event ACTION", slogx.Stringer("txHash", (event.Transaction.TxHash)))
}
}
// close transaction
err = qtx.Commit(ctx)
err = tx.Commit(ctx)
if err != nil {
return errors.Wrap(err, "Failed to commit transaction")
return fmt.Errorf("Failed to commit transaction : %w", err)
}
logger.InfoContext(ctx, "NodeSale finished processing block",
slogx.Int64("block", block.Header.Height),
slogx.Stringer("hash", block.Header.Hash))
logger.InfoContext(ctx, "Nodesale finished processing block",
slog.Int64("block", block.Header.Height),
slog.String("hash", block.Header.Hash.String()))
}
return nil
}
// RevertData implements indexer.Processor.
func (p *Processor) RevertData(ctx context.Context, from int64) error {
qtx, err := p.NodeSaleDg.BeginNodeSaleTx(ctx)
tx, err := p.repository.Db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create transaction")
return fmt.Errorf("Failed to create transaction : %w", err)
}
defer func() { err = qtx.Rollback(ctx) }()
_, err = qtx.RemoveBlockFrom(ctx, from)
defer tx.Rollback(ctx)
qtx := p.repository.WithTx(tx)
_, err = qtx.RemoveBlockFrom(ctx, int32(from))
if err != nil {
return errors.Wrap(err, "Failed to remove blocks.")
return fmt.Errorf("Failed to remove blocks. : %w", err)
}
affected, err := qtx.RemoveEventsFromBlock(ctx, from)
affected, err := qtx.RemoveEventsFromBlock(ctx, int32(from))
if err != nil {
return errors.Wrap(err, "Failed to remove events.")
return fmt.Errorf("Failed to remove events. : %w", err)
}
_, err = qtx.ClearDelegate(ctx)
if err != nil {
return errors.Wrap(err, "Failed to clear delegate from nodes")
return fmt.Errorf("Failed to clear delegate from nodes : %w", err)
}
err = qtx.Commit(ctx)
err = tx.Commit(ctx)
if err != nil {
return errors.Wrap(err, "Failed to commit transaction")
return fmt.Errorf("Failed to commit transaction : %w", err)
}
logger.InfoContext(ctx, "Events removed",
slogx.Int64("Total removed", affected))
slog.Int("Total removed", int(affected)))
return nil
}
@@ -300,4 +277,14 @@ func (p *Processor) VerifyStates(ctx context.Context) error {
panic("unimplemented")
}
func (p *Processor) Shutdown(ctx context.Context) error {
for _, cleanupFunc := range p.cleanupFuncs {
err := cleanupFunc(ctx)
if err != nil {
return fmt.Errorf("cleanup function error : %w", err)
}
}
return nil
}
var _ indexer.Processor[*types.Block] = (*Processor)(nil)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,19 @@ import (
"context"
)
const createBlock = `-- name: CreateBlock :exec
INSERT INTO blocks ("block_height", "block_hash", "module")
const addBlock = `-- name: AddBlock :exec
INSERT INTO blocks("block_height", "block_hash", "module")
VALUES ($1, $2, $3)
`
type CreateBlockParams struct {
BlockHeight int64
type AddBlockParams struct {
BlockHeight int32
BlockHash string
Module string
}
func (q *Queries) CreateBlock(ctx context.Context, arg CreateBlockParams) error {
_, err := q.db.Exec(ctx, createBlock, arg.BlockHeight, arg.BlockHash, arg.Module)
func (q *Queries) AddBlock(ctx context.Context, arg AddBlockParams) error {
_, err := q.db.Exec(ctx, addBlock, arg.BlockHeight, arg.BlockHash, arg.Module)
return err
}
@@ -30,7 +30,7 @@ SELECT block_height, block_hash, module FROM blocks
WHERE "block_height" = $1
`
func (q *Queries) GetBlock(ctx context.Context, blockHeight int64) (Block, error) {
func (q *Queries) GetBlock(ctx context.Context, blockHeight int32) (Block, error) {
row := q.db.QueryRow(ctx, getBlock, blockHeight)
var i Block
err := row.Scan(&i.BlockHeight, &i.BlockHash, &i.Module)
@@ -38,7 +38,8 @@ func (q *Queries) GetBlock(ctx context.Context, blockHeight int64) (Block, error
}
const getLastProcessedBlock = `-- name: GetLastProcessedBlock :one
SELECT block_height, block_hash, module FROM blocks ORDER BY block_height DESC LIMIT 1
SELECT block_height, block_hash, module FROM blocks
WHERE "block_height" = (SELECT MAX("block_height") FROM blocks)
`
func (q *Queries) GetLastProcessedBlock(ctx context.Context) (Block, error) {
@@ -53,7 +54,7 @@ DELETE FROM blocks
WHERE "block_height" >= $1
`
func (q *Queries) RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error) {
func (q *Queries) RemoveBlockFrom(ctx context.Context, fromBlock int32) (int64, error) {
result, err := q.db.Exec(ctx, removeBlockFrom, fromBlock)
if err != nil {
return 0, err

View File

@@ -11,16 +11,15 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const createEvent = `-- name: CreateEvent :exec
INSERT INTO events ("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata",
"reason")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
const addEvent = `-- name: AddEvent :exec
INSERT INTO events("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type CreateEventParams struct {
type AddEventParams struct {
TxHash string
BlockHeight int64
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
@@ -30,11 +29,10 @@ type CreateEventParams struct {
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
Reason string
}
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) error {
_, err := q.db.Exec(ctx, createEvent,
func (q *Queries) AddEvent(ctx context.Context, arg AddEventParams) error {
_, err := q.db.Exec(ctx, addEvent,
arg.TxHash,
arg.BlockHeight,
arg.TxIndex,
@@ -46,13 +44,12 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) error
arg.BlockTimestamp,
arg.BlockHash,
arg.Metadata,
arg.Reason,
)
return err
}
const getEventsByWallet = `-- name: GetEventsByWallet :many
SELECT tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata, reason
SELECT tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata
FROM events
WHERE wallet_address = $1
`
@@ -78,7 +75,6 @@ func (q *Queries) GetEventsByWallet(ctx context.Context, walletAddress string) (
&i.BlockTimestamp,
&i.BlockHash,
&i.Metadata,
&i.Reason,
); err != nil {
return nil, err
}
@@ -95,7 +91,7 @@ DELETE FROM events
WHERE "block_height" >= $1
`
func (q *Queries) RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error) {
func (q *Queries) RemoveEventsFromBlock(ctx context.Context, fromBlock int32) (int64, error) {
result, err := q.db.Exec(ctx, removeEventsFromBlock, fromBlock)
if err != nil {
return 0, err

View File

@@ -9,14 +9,14 @@ import (
)
type Block struct {
BlockHeight int64
BlockHeight int32
BlockHash string
Module string
}
type Event struct {
TxHash string
BlockHeight int64
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
@@ -26,11 +26,10 @@ type Event struct {
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
Reason string
}
type Node struct {
SaleBlock int64
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
@@ -41,7 +40,7 @@ type Node struct {
}
type NodeSale struct {
BlockHeight int64
BlockHeight int32
TxIndex int32
Name string
StartsAt pgtype.Timestamp

View File

@@ -7,29 +7,17 @@ package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const clearDelegate = `-- name: ClearDelegate :execrows
UPDATE nodes
SET "delegated_to" = ''
WHERE "delegate_tx_hash" = ''
`
func (q *Queries) ClearDelegate(ctx context.Context) (int64, error) {
result, err := q.db.Exec(ctx, clearDelegate)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const createNode = `-- name: CreateNode :exec
INSERT INTO nodes (sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
const addNode = `-- name: AddNode :exec
INSERT INTO nodes(sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
type CreateNodeParams struct {
SaleBlock int64
type AddNodeParams struct {
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
@@ -39,8 +27,8 @@ type CreateNodeParams struct {
DelegateTxHash string
}
func (q *Queries) CreateNode(ctx context.Context, arg CreateNodeParams) error {
_, err := q.db.Exec(ctx, createNode,
func (q *Queries) AddNode(ctx context.Context, arg AddNodeParams) error {
_, err := q.db.Exec(ctx, addNode,
arg.SaleBlock,
arg.SaleTxIndex,
arg.NodeID,
@@ -53,28 +41,42 @@ func (q *Queries) CreateNode(ctx context.Context, arg CreateNodeParams) error {
return err
}
const clearDelegate = `-- name: ClearDelegate :execrows
UPDATE nodes
SET "delegated_to" = ''
WHERE "delegate_tx_hash" = NULL
`
func (q *Queries) ClearDelegate(ctx context.Context) (int64, error) {
result, err := q.db.Exec(ctx, clearDelegate)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getNodeCountByTierIndex = `-- name: GetNodeCountByTierIndex :many
SELECT (tiers.tier_index)::int AS tier_index, count(nodes.tier_index)
FROM generate_series($3::int,$4::int) AS tiers(tier_index)
SELECT tiers.tier_index as tier_index, count(nodes.tier_index)
FROM generate_series($3::int,$4::int) as tiers(tier_index)
LEFT JOIN
(SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
FROM nodes
WHERE sale_block = $1 AND
(select sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
from nodes
where sale_block = $1 and
sale_tx_index= $2)
AS nodes ON tiers.tier_index = nodes.tier_index
GROUP BY tiers.tier_index
as nodes on tiers.tier_index = nodes.tier_index
group by tiers.tier_index
ORDER BY tiers.tier_index
`
type GetNodeCountByTierIndexParams struct {
SaleBlock int64
SaleBlock int32
SaleTxIndex int32
FromTier int32
ToTier int32
}
type GetNodeCountByTierIndexRow struct {
TierIndex int32
TierIndex interface{}
Count int64
}
@@ -103,48 +105,7 @@ func (q *Queries) GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountB
return items, nil
}
const getNodesByDeployment = `-- name: GetNodesByDeployment :many
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
FROM nodes
WHERE sale_block = $1 AND
sale_tx_index = $2
`
type GetNodesByDeploymentParams struct {
SaleBlock int64
SaleTxIndex int32
}
func (q *Queries) GetNodesByDeployment(ctx context.Context, arg GetNodesByDeploymentParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodesByDeployment, arg.SaleBlock, arg.SaleTxIndex)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Node
for rows.Next() {
var i Node
if err := rows.Scan(
&i.SaleBlock,
&i.SaleTxIndex,
&i.NodeID,
&i.TierIndex,
&i.DelegatedTo,
&i.OwnerPublicKey,
&i.PurchaseTxHash,
&i.DelegateTxHash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNodesByIds = `-- name: GetNodesByIds :many
const getNodes = `-- name: GetNodes :many
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
FROM nodes
WHERE sale_block = $1 AND
@@ -152,14 +113,14 @@ WHERE sale_block = $1 AND
node_id = ANY ($3::int[])
`
type GetNodesByIdsParams struct {
SaleBlock int64
type GetNodesParams struct {
SaleBlock int32
SaleTxIndex int32
NodeIds []int32
}
func (q *Queries) GetNodesByIds(ctx context.Context, arg GetNodesByIdsParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodesByIds, arg.SaleBlock, arg.SaleTxIndex, arg.NodeIds)
func (q *Queries) GetNodes(ctx context.Context, arg GetNodesParams) ([]Node, error) {
rows, err := q.db.Query(ctx, getNodes, arg.SaleBlock, arg.SaleTxIndex, arg.NodeIds)
if err != nil {
return nil, err
}
@@ -197,7 +158,7 @@ ORDER BY tier_index
`
type GetNodesByOwnerParams struct {
SaleBlock int64
SaleBlock int32
SaleTxIndex int32
OwnerPublicKey string
}
@@ -232,7 +193,7 @@ func (q *Queries) GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams
}
const getNodesByPubkey = `-- name: GetNodesByPubkey :many
SELECT nodes.sale_block, nodes.sale_tx_index, nodes.node_id, nodes.tier_index, nodes.delegated_to, nodes.owner_public_key, nodes.purchase_tx_hash, nodes.delegate_tx_hash
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash, tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata
FROM nodes JOIN events ON nodes.purchase_tx_hash = events.tx_hash
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
@@ -241,13 +202,35 @@ WHERE sale_block = $1 AND
`
type GetNodesByPubkeyParams struct {
SaleBlock int64
SaleBlock int32
SaleTxIndex int32
OwnerPublicKey string
DelegatedTo string
}
func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]Node, error) {
type GetNodesByPubkeyRow struct {
SaleBlock int32
SaleTxIndex int32
NodeID int32
TierIndex int32
DelegatedTo string
OwnerPublicKey string
PurchaseTxHash string
DelegateTxHash string
TxHash string
BlockHeight int32
TxIndex int32
WalletAddress string
Valid bool
Action int32
RawMessage []byte
ParsedMessage []byte
BlockTimestamp pgtype.Timestamp
BlockHash string
Metadata []byte
}
func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]GetNodesByPubkeyRow, error) {
rows, err := q.db.Query(ctx, getNodesByPubkey,
arg.SaleBlock,
arg.SaleTxIndex,
@@ -258,9 +241,9 @@ func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyPara
return nil, err
}
defer rows.Close()
var items []Node
var items []GetNodesByPubkeyRow
for rows.Next() {
var i Node
var i GetNodesByPubkeyRow
if err := rows.Scan(
&i.SaleBlock,
&i.SaleTxIndex,
@@ -270,6 +253,17 @@ func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyPara
&i.OwnerPublicKey,
&i.PurchaseTxHash,
&i.DelegateTxHash,
&i.TxHash,
&i.BlockHeight,
&i.TxIndex,
&i.WalletAddress,
&i.Valid,
&i.Action,
&i.RawMessage,
&i.ParsedMessage,
&i.BlockTimestamp,
&i.BlockHash,
&i.Metadata,
); err != nil {
return nil, err
}
@@ -283,25 +277,23 @@ func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyPara
const setDelegates = `-- name: SetDelegates :execrows
UPDATE nodes
SET delegated_to = $4, delegate_tx_hash = $3
SET delegated_to = $3
WHERE sale_block = $1 AND
sale_tx_index = $2 AND
node_id = ANY ($5::int[])
node_id = ANY ($4::int[])
`
type SetDelegatesParams struct {
SaleBlock int64
SaleTxIndex int32
DelegateTxHash string
Delegatee string
NodeIds []int32
SaleBlock int32
SaleTxIndex int32
Delegatee string
NodeIds []int32
}
func (q *Queries) SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error) {
result, err := q.db.Exec(ctx, setDelegates,
arg.SaleBlock,
arg.SaleTxIndex,
arg.DelegateTxHash,
arg.Delegatee,
arg.NodeIds,
)

View File

@@ -11,13 +11,13 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const createNodeSale = `-- name: CreateNodeSale :exec
INSERT INTO node_sales ("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
const addNodesale = `-- name: AddNodesale :exec
INSERT INTO node_sales("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type CreateNodeSaleParams struct {
BlockHeight int64
type AddNodesaleParams struct {
BlockHeight int32
TxIndex int32
Name string
StartsAt pgtype.Timestamp
@@ -30,8 +30,8 @@ type CreateNodeSaleParams struct {
SellerWallet string
}
func (q *Queries) CreateNodeSale(ctx context.Context, arg CreateNodeSaleParams) error {
_, err := q.db.Exec(ctx, createNodeSale,
func (q *Queries) AddNodesale(ctx context.Context, arg AddNodesaleParams) error {
_, err := q.db.Exec(ctx, addNodesale,
arg.BlockHeight,
arg.TxIndex,
arg.Name,
@@ -47,20 +47,20 @@ func (q *Queries) CreateNodeSale(ctx context.Context, arg CreateNodeSaleParams)
return err
}
const getNodeSale = `-- name: GetNodeSale :many
const getNodesale = `-- name: GetNodesale :many
SELECT block_height, tx_index, name, starts_at, ends_at, tiers, seller_public_key, max_per_address, deploy_tx_hash, max_discount_percentage, seller_wallet
FROM node_sales
WHERE block_height = $1 AND
tx_index = $2
`
type GetNodeSaleParams struct {
BlockHeight int64
type GetNodesaleParams struct {
BlockHeight int32
TxIndex int32
}
func (q *Queries) GetNodeSale(ctx context.Context, arg GetNodeSaleParams) ([]NodeSale, error) {
rows, err := q.db.Query(ctx, getNodeSale, arg.BlockHeight, arg.TxIndex)
func (q *Queries) GetNodesale(ctx context.Context, arg GetNodesaleParams) ([]NodeSale, error) {
rows, err := q.db.Query(ctx, getNodesale, arg.BlockHeight, arg.TxIndex)
if err != nil {
return nil, err
}

View File

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

View File

@@ -1,74 +0,0 @@
package postgres
import (
"encoding/json"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/samber/lo"
)
func mapNodes(nodes []gen.Node) []entity.Node {
return lo.Map(nodes, func(item gen.Node, index int) entity.Node {
return entity.Node{
SaleBlock: uint64(item.SaleBlock),
SaleTxIndex: uint32(item.SaleTxIndex),
NodeID: uint32(item.NodeID),
TierIndex: item.TierIndex,
DelegatedTo: item.DelegatedTo,
OwnerPublicKey: item.OwnerPublicKey,
PurchaseTxHash: item.PurchaseTxHash,
DelegateTxHash: item.DelegateTxHash,
}
})
}
func mapNodeSales(nodeSales []gen.NodeSale) []entity.NodeSale {
return lo.Map(nodeSales, func(item gen.NodeSale, index int) entity.NodeSale {
return entity.NodeSale{
BlockHeight: uint64(item.BlockHeight),
TxIndex: uint32(item.TxIndex),
Name: item.Name,
StartsAt: item.StartsAt.Time,
EndsAt: item.EndsAt.Time,
Tiers: item.Tiers,
SellerPublicKey: item.SellerPublicKey,
MaxPerAddress: uint32(item.MaxPerAddress),
DeployTxHash: item.DeployTxHash,
MaxDiscountPercentage: item.MaxDiscountPercentage,
SellerWallet: item.SellerWallet,
}
})
}
func mapNodeCountByTierIndexRows(nodeCount []gen.GetNodeCountByTierIndexRow) []datagateway.GetNodeCountByTierIndexRow {
return lo.Map(nodeCount, func(item gen.GetNodeCountByTierIndexRow, index int) datagateway.GetNodeCountByTierIndexRow {
return datagateway.GetNodeCountByTierIndexRow{
TierIndex: item.TierIndex,
}
})
}
func mapNodeSalesEvents(events []gen.Event) []entity.NodeSaleEvent {
return lo.Map(events, func(item gen.Event, index int) entity.NodeSaleEvent {
var meta entity.MetadataEventPurchase
err := json.Unmarshal(item.Metadata, &meta)
if err != nil {
meta = entity.MetadataEventPurchase{}
}
return entity.NodeSaleEvent{
TxHash: item.TxHash,
BlockHeight: item.BlockHeight,
TxIndex: item.TxIndex,
WalletAddress: item.WalletAddress,
Valid: item.Valid,
Action: item.Action,
RawMessage: item.RawMessage,
ParsedMessage: item.ParsedMessage,
BlockTimestamp: item.BlockTimestamp.Time.UTC(),
BlockHash: item.BlockHash,
Metadata: &meta,
}
})
}

View File

@@ -1,247 +1,24 @@
package postgres
import (
"context"
"encoding/json"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
db "github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
)
type Repository struct {
db postgres.DB
queries *gen.Queries
tx pgx.Tx
Db db.TxQueryable
Queries gen.Querier
}
func NewRepository(db postgres.DB) *Repository {
func NewRepository(db db.DB) *Repository {
return &Repository{
db: db,
queries: gen.New(db),
Db: db,
Queries: gen.New(db),
}
}
func (repo *Repository) CreateBlock(ctx context.Context, arg entity.Block) error {
err := repo.queries.CreateBlock(ctx, gen.CreateBlockParams{
BlockHeight: arg.BlockHeight,
BlockHash: arg.BlockHash,
Module: arg.Module,
})
if err != nil {
return errors.Wrap(err, "Cannot Add block")
}
return nil
}
func (repo *Repository) GetBlock(ctx context.Context, blockHeight int64) (*entity.Block, error) {
block, err := repo.queries.GetBlock(ctx, blockHeight)
if err != nil {
return nil, errors.Wrap(err, "Cannot get block")
}
return &entity.Block{
BlockHeight: block.BlockHeight,
BlockHash: block.BlockHash,
Module: block.Module,
}, nil
}
func (repo *Repository) GetLastProcessedBlock(ctx context.Context) (*entity.Block, error) {
block, err := repo.queries.GetLastProcessedBlock(ctx)
if err != nil {
return nil, errors.Wrap(err, "Cannot get last processed block")
}
return &entity.Block{
BlockHeight: block.BlockHeight,
BlockHash: block.BlockHash,
Module: block.Module,
}, nil
}
func (repo *Repository) RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error) {
affected, err := repo.queries.RemoveBlockFrom(ctx, fromBlock)
if err != nil {
return 0, errors.Wrap(err, "Cannot remove blocks")
}
return affected, nil
}
func (repo *Repository) RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error) {
affected, err := repo.queries.RemoveEventsFromBlock(ctx, fromBlock)
if err != nil {
return 0, errors.Wrap(err, "Cannot remove events")
}
return affected, nil
}
func (repo *Repository) ClearDelegate(ctx context.Context) (int64, error) {
affected, err := repo.queries.ClearDelegate(ctx)
if err != nil {
return 0, errors.Wrap(err, "Cannot clear delegate")
}
return affected, nil
}
func (repo *Repository) GetNodesByIds(ctx context.Context, arg datagateway.GetNodesByIdsParams) ([]entity.Node, error) {
nodes, err := repo.queries.GetNodesByIds(ctx, gen.GetNodesByIdsParams{
SaleBlock: int64(arg.SaleBlock),
SaleTxIndex: int32(arg.SaleTxIndex),
NodeIds: lo.Map(arg.NodeIds, func(item uint32, index int) int32 { return int32(item) }),
})
if err != nil {
return nil, errors.Wrap(err, "Cannot get nodes")
}
return mapNodes(nodes), nil
}
func (repo *Repository) CreateEvent(ctx context.Context, arg entity.NodeSaleEvent) error {
metaDataBytes := []byte("{}")
if arg.Metadata != nil {
metaDataBytes, _ = json.Marshal(arg.Metadata)
}
err := repo.queries.CreateEvent(ctx, gen.CreateEventParams{
TxHash: arg.TxHash,
BlockHeight: arg.BlockHeight,
TxIndex: arg.TxIndex,
WalletAddress: arg.WalletAddress,
Valid: arg.Valid,
Action: arg.Action,
RawMessage: arg.RawMessage,
ParsedMessage: arg.ParsedMessage,
BlockTimestamp: pgtype.Timestamp{Time: arg.BlockTimestamp.UTC(), Valid: true},
BlockHash: arg.BlockHash,
Metadata: metaDataBytes,
Reason: arg.Reason,
})
if err != nil {
return errors.Wrap(err, "Cannot add event")
}
return nil
}
func (repo *Repository) SetDelegates(ctx context.Context, arg datagateway.SetDelegatesParams) (int64, error) {
affected, err := repo.queries.SetDelegates(ctx, gen.SetDelegatesParams{
SaleBlock: int64(arg.SaleBlock),
SaleTxIndex: arg.SaleTxIndex,
Delegatee: arg.Delegatee,
DelegateTxHash: arg.DelegateTxHash,
NodeIds: lo.Map(arg.NodeIds, func(item uint32, index int) int32 { return int32(item) }),
})
if err != nil {
return 0, errors.Wrap(err, "Cannot set delegate")
}
return affected, nil
}
func (repo *Repository) CreateNodeSale(ctx context.Context, arg entity.NodeSale) error {
err := repo.queries.CreateNodeSale(ctx, gen.CreateNodeSaleParams{
BlockHeight: int64(arg.BlockHeight),
TxIndex: int32(arg.TxIndex),
Name: arg.Name,
StartsAt: pgtype.Timestamp{Time: arg.StartsAt.UTC(), Valid: true},
EndsAt: pgtype.Timestamp{Time: arg.EndsAt.UTC(), Valid: true},
Tiers: arg.Tiers,
SellerPublicKey: arg.SellerPublicKey,
MaxPerAddress: int32(arg.MaxPerAddress),
DeployTxHash: arg.DeployTxHash,
MaxDiscountPercentage: arg.MaxDiscountPercentage,
SellerWallet: arg.SellerWallet,
})
if err != nil {
return errors.Wrap(err, "Cannot add NodeSale")
}
return nil
}
func (repo *Repository) GetNodeSale(ctx context.Context, arg datagateway.GetNodeSaleParams) ([]entity.NodeSale, error) {
nodeSales, err := repo.queries.GetNodeSale(ctx, gen.GetNodeSaleParams{
BlockHeight: int64(arg.BlockHeight),
TxIndex: int32(arg.TxIndex),
})
if err != nil {
return nil, errors.Wrap(err, "Cannot get NodeSale")
}
return mapNodeSales(nodeSales), nil
}
func (repo *Repository) GetNodesByOwner(ctx context.Context, arg datagateway.GetNodesByOwnerParams) ([]entity.Node, error) {
nodes, err := repo.queries.GetNodesByOwner(ctx, gen.GetNodesByOwnerParams{
SaleBlock: int64(arg.SaleBlock),
SaleTxIndex: int32(arg.SaleTxIndex),
OwnerPublicKey: arg.OwnerPublicKey,
})
if err != nil {
return nil, errors.Wrap(err, "Cannot get nodes by owner")
}
return mapNodes(nodes), nil
}
func (repo *Repository) CreateNode(ctx context.Context, arg entity.Node) error {
err := repo.queries.CreateNode(ctx, gen.CreateNodeParams{
SaleBlock: int64(arg.SaleBlock),
SaleTxIndex: int32(arg.SaleTxIndex),
NodeID: int32(arg.NodeID),
TierIndex: arg.TierIndex,
DelegatedTo: arg.DelegatedTo,
OwnerPublicKey: arg.OwnerPublicKey,
PurchaseTxHash: arg.PurchaseTxHash,
DelegateTxHash: arg.DelegateTxHash,
})
if err != nil {
return errors.Wrap(err, "Cannot add node")
}
return nil
}
func (repo *Repository) GetNodeCountByTierIndex(ctx context.Context, arg datagateway.GetNodeCountByTierIndexParams) ([]datagateway.GetNodeCountByTierIndexRow, error) {
nodeCount, err := repo.queries.GetNodeCountByTierIndex(ctx, gen.GetNodeCountByTierIndexParams{
SaleBlock: int64(arg.SaleBlock),
SaleTxIndex: int32(arg.SaleTxIndex),
FromTier: int32(arg.FromTier),
ToTier: int32(arg.ToTier),
})
if err != nil {
return nil, errors.Wrap(err, "Cannot get node count by tier index")
}
return mapNodeCountByTierIndexRows(nodeCount), nil
}
func (repo *Repository) GetNodesByPubkey(ctx context.Context, arg datagateway.GetNodesByPubkeyParams) ([]entity.Node, error) {
nodes, err := repo.queries.GetNodesByPubkey(ctx, gen.GetNodesByPubkeyParams{
SaleBlock: arg.SaleBlock,
SaleTxIndex: arg.SaleTxIndex,
OwnerPublicKey: arg.OwnerPublicKey,
DelegatedTo: arg.DelegatedTo,
})
if err != nil {
return nil, errors.Wrap(err, "Cannot get nodes by public key")
}
return mapNodes(nodes), nil
}
func (repo *Repository) GetEventsByWallet(ctx context.Context, walletAddress string) ([]entity.NodeSaleEvent, error) {
events, err := repo.queries.GetEventsByWallet(ctx, walletAddress)
if err != nil {
return nil, errors.Wrap(err, "cannot get events by wallet")
}
return mapNodeSalesEvents(events), nil
}
func (repo *Repository) GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error) {
nodes, err := repo.queries.GetNodesByDeployment(ctx, gen.GetNodesByDeploymentParams{
SaleBlock: saleBlock,
SaleTxIndex: saleTxIndex,
})
if err != nil {
return nil, errors.Wrap(err, "cannot get nodes by deploy")
}
return mapNodes(nodes), nil
func (q *Repository) WithTx(tx pgx.Tx) gen.Querier {
queries := gen.Queries{}
return queries.WithTx(tx)
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
package httphandler
import (
"bytes"
"encoding/hex"
"slices"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
"github.com/gofiber/fiber/v2"
@@ -17,26 +14,13 @@ import (
type getHoldersRequest struct {
Id string `params:"id"`
BlockHeight uint64 `query:"blockHeight"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
const (
getHoldersMaxLimit = 1000
getHoldersDefaultLimit = 100
)
func (r getHoldersRequest) Validate() error {
var errList []error
if !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getHoldersMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getHoldersMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -77,10 +61,6 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
blockHeight = uint64(blockHeader.Height)
}
if req.Limit == 0 {
req.Limit = getHoldersDefaultLimit
}
var runeId runes.RuneId
if req.Id != "" {
var ok bool
@@ -92,16 +72,10 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
runeEntry, err := h.usecase.GetRuneEntryByRuneIdAndHeight(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
return errors.Wrap(err, "error during GetHoldersByHeight")
}
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, req.Limit, req.Offset)
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("balances not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
}
@@ -127,14 +101,6 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
})
}
// sort by amount descending, then pk script ascending
slices.SortFunc(holdingBalances, func(b1, b2 *entity.Balance) int {
if b1.Amount.Cmp(b2.Amount) == 0 {
return bytes.Compare(b1.PkScript, b2.PkScript)
}
return b2.Amount.Cmp(b1.Amount)
})
resp := getHoldersResponse{
Result: &getHoldersResult{
BlockHeight: blockHeight,

View File

@@ -83,9 +83,6 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
@@ -102,16 +99,10 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
runeEntry, err := h.usecase.GetRuneEntryByRuneIdAndHeight(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetTokenInfoByHeight")
}
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune not found")
}
return errors.Wrap(err, "error during GetBalancesByRuneId")
}

View File

@@ -1,9 +1,7 @@
package httphandler
import (
"cmp"
"encoding/hex"
"fmt"
"slices"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -16,36 +14,16 @@ import (
)
type getTransactionsRequest struct {
Wallet string `query:"wallet"`
Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
ToBlock int64 `query:"toBlock"`
Limit int32 `query:"limit"`
Offset int32 `query:"offset"`
Wallet string `query:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
const (
getTransactionsMaxLimit = 3000
getTransactionsDefaultLimit = 100
)
func (r getTransactionsRequest) Validate() error {
var errList []error
if r.Id != "" && !isRuneIdOrRuneName(r.Id) {
errList = append(errList, errors.New("'id' is not valid rune id or rune name"))
}
if r.FromBlock < -1 {
errList = append(errList, errors.Errorf("invalid fromBlock range"))
}
if r.ToBlock < -1 {
errList = append(errList, errors.Errorf("invalid toBlock range"))
}
if r.Limit < 0 {
errList = append(errList, errors.New("'limit' must be non-negative"))
}
if r.Limit > getTransactionsMaxLimit {
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getTransactionsMaxLimit))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -146,42 +124,19 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errs.NewPublicError("unable to resolve rune id from \"id\"")
}
}
if req.Limit == 0 {
req.Limit = getTransactionsDefaultLimit
}
// default to latest block
if req.ToBlock == 0 {
req.ToBlock = -1
}
// get latest block height if block height is -1
if req.FromBlock == -1 || req.ToBlock == -1 {
blockHeight := req.BlockHeight
// set blockHeight to the latest block height blockHeight, pkScript, and runeId are not provided
if blockHeight == 0 && pkScript == nil && runeId == (runes.RuneId{}) {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("latest block not found")
}
return errors.Wrap(err, "error during GetLatestBlock")
}
if req.FromBlock == -1 {
req.FromBlock = blockHeader.Height
}
if req.ToBlock == -1 {
req.ToBlock = blockHeader.Height
}
blockHeight = uint64(blockHeader.Height)
}
// validate block height range
if req.FromBlock > req.ToBlock {
return errs.NewPublicError(fmt.Sprintf("fromBlock must be less than or equal to toBlock, got fromBlock=%d, toBlock=%d", req.FromBlock, req.ToBlock))
}
txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, uint64(req.FromBlock), uint64(req.ToBlock), req.Limit, req.Offset)
txs, err := h.usecase.GetRuneTransactions(ctx.UserContext(), pkScript, runeId, blockHeight)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("transactions not found")
}
return errors.Wrap(err, "error during GetRuneTransactions")
}
@@ -203,9 +158,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
allRuneIds = lo.Uniq(allRuneIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds)
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")
}
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
@@ -304,12 +256,12 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
}
txList = append(txList, respTx)
}
// sort by block height DESC, then index DESC
// sort by block height ASC, then index ASC
slices.SortFunc(txList, func(t1, t2 transaction) int {
if t1.BlockHeight != t2.BlockHeight {
return cmp.Compare(t2.BlockHeight, t1.BlockHeight)
return int(t1.BlockHeight - t2.BlockHeight)
}
return cmp.Compare(t2.Index, t1.Index)
return int(t1.Index - t2.Index)
})
resp := getTransactionsResponse{

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,12 @@ import (
func (h *HttpHandler) Mount(router fiber.Router) error {
r := router.Group("/v2/runes")
r.Post("/balances/wallet/batch", h.GetBalancesBatch)
r.Get("/balances/wallet/:wallet", h.GetBalances)
r.Post("/balances/wallet/batch", h.GetBalancesByAddressBatch)
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress)
r.Get("/transactions", h.GetTransactions)
r.Get("/holders/:id", h.GetHolders)
r.Get("/info/:id", h.GetTokenInfo)
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
r.Get("/utxos/output/:txHash", h.GetUTXOsOutputByLocation)
r.Get("/utxos/wallet/:wallet", h.GetUTXOsByAddress)
r.Get("/block", h.GetCurrentBlock)
return nil
}

View File

@@ -118,7 +118,5 @@ CREATE TABLE IF NOT EXISTS "runes_balances" (
"amount" DECIMAL NOT NULL,
PRIMARY KEY ("pkscript", "rune_id", "block_height")
);
CREATE INDEX IF NOT EXISTS runes_balances_rune_id_block_height_idx ON "runes_balances" USING BTREE ("rune_id", "block_height");
CREATE INDEX IF NOT EXISTS runes_balances_pkscript_block_height_idx ON "runes_balances" USING BTREE ("pkscript", "block_height");
COMMIT;

View File

@@ -2,13 +2,13 @@
WITH balances AS (
SELECT DISTINCT ON (rune_id) * FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
)
SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4;
SELECT * FROM balances WHERE amount > 0;
-- name: GetBalancesByRuneId :many
WITH balances AS (
SELECT DISTINCT ON (pkscript) * FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
)
SELECT * FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4;
SELECT * FROM balances WHERE amount > 0;
-- name: GetBalanceByPkScriptAndRuneId :one
SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_height <= $3 ORDER BY block_height DESC LIMIT 1;
@@ -16,28 +16,8 @@ SELECT * FROM runes_balances WHERE pkscript = $1 AND rune_id = $2 AND block_heig
-- name: GetOutPointBalancesAtOutPoint :many
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
-- name: GetRunesUTXOsByPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = @pkScript AND
block_height <= @block_height AND
(spent_height IS NULL OR spent_height > @block_height)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> @rune_ids::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2;
-- name: GetUnspentOutPointBalancesByPkScript :many
SELECT * FROM runes_outpoint_balances WHERE pkscript = @pkScript AND block_height <= @block_height AND (spent_height IS NULL OR spent_height > @block_height);
-- name: GetRuneEntriesByRuneIds :many
WITH states AS (
@@ -75,14 +55,8 @@ SELECT * FROM runes_transactions
OR runes_transactions.burns ? @rune_id
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = @rune_id_block_height AND runes_transactions.index = @rune_id_tx_index)
) AND (
@from_block <= runes_transactions.block_height AND runes_transactions.block_height <= @to_block
)
ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2;
-- name: GetRuneTransaction :one
SELECT * FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE hash = $1 LIMIT 1;
@block_height::INT = 0 OR runes_transactions.block_height = @block_height::INT -- if @block_height > 0, apply block_height filter
);
-- name: CountRuneEntries :one
SELECT COUNT(*) FROM runes_entries;

View File

@@ -3,7 +3,6 @@ package datagateway
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
@@ -28,12 +27,10 @@ type RunesReaderDataGateway interface {
GetLatestBlock(ctx context.Context) (types.BlockHeader, error)
GetIndexedBlockByHeight(ctx context.Context, height int64) (*entity.IndexedBlock, error)
// GetRuneTransactions returns the runes transactions, filterable by pkScript, runeId and height. If pkScript, runeId or height is zero value, that filter is ignored.
GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error)
GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error)
GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, height uint64) ([]*entity.RuneTransaction, error)
GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error)
GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error)
GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error)
// GetRuneIdFromRune returns the RuneId for the given rune. Returns errs.NotFound if the rune entry is not found.
GetRuneIdFromRune(ctx context.Context, rune runes.Rune) (runes.RuneId, error)
// GetRuneEntryByRuneId returns the RuneEntry for the given runeId. Returns errs.NotFound if the rune entry is not found.
@@ -48,12 +45,10 @@ type RunesReaderDataGateway interface {
CountRuneEntries(ctx context.Context) (uint64, error)
// GetBalancesByPkScript returns the balances for the given pkScript at the given blockHeight.
// Use limit = -1 as no limit.
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error)
// GetBalancesByRuneId returns the balances for the given runeId at the given blockHeight.
// Cannot use []byte as map key, so we're returning as slice.
// Use limit = -1 as no limit.
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error)
GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error)
// GetBalancesByPkScriptAndRuneId returns the balance for the given pkScript and runeId at the given blockHeight.
GetBalanceByPkScriptAndRuneId(ctx context.Context, pkScript []byte, runeId runes.RuneId, blockHeight uint64) (*entity.Balance, error)
}

View File

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

View File

@@ -146,7 +146,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
// find all non-OP_RETURN outputs
var destinations []int
for i, txOut := range tx.TxOut {
if !txOut.IsOpReturn() {
if txOut.IsOpReturn() {
destinations = append(destinations, i)
}
}
@@ -466,7 +466,7 @@ func (p *Processor) txCommitsToRune(ctx context.Context, tx *types.Transaction,
// It is impossible to verify that input utxo is a P2TR output with just the input.
// Need to verify with utxo's pk script.
prevTx, blockHeight, err := p.bitcoinClient.GetRawTransactionAndHeightByTxHash(ctx, txIn.PreviousOutTxHash)
prevTx, err := p.bitcoinClient.GetTransactionByHash(ctx, txIn.PreviousOutTxHash)
if err != nil && errors.Is(err, errs.NotFound) {
continue
}
@@ -479,7 +479,7 @@ func (p *Processor) txCommitsToRune(ctx context.Context, tx *types.Transaction,
break
}
// input must be mature enough
confirmations := tx.BlockHeight - blockHeight + 1
confirmations := tx.BlockHeight - prevTx.BlockHeight + 1
if confirmations < runes.RUNE_COMMIT_BLOCKS {
continue
}

View File

@@ -296,14 +296,12 @@ const getBalancesByPkScript = `-- name: GetBalancesByPkScript :many
WITH balances AS (
SELECT DISTINCT ON (rune_id) pkscript, block_height, rune_id, amount FROM runes_balances WHERE pkscript = $1 AND block_height <= $2 ORDER BY rune_id, block_height DESC
)
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, rune_id LIMIT $3 OFFSET $4
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0
`
type GetBalancesByPkScriptParams struct {
Pkscript string
BlockHeight int32
Limit int32
Offset int32
}
type GetBalancesByPkScriptRow struct {
@@ -314,12 +312,7 @@ type GetBalancesByPkScriptRow struct {
}
func (q *Queries) GetBalancesByPkScript(ctx context.Context, arg GetBalancesByPkScriptParams) ([]GetBalancesByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getBalancesByPkScript,
arg.Pkscript,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
rows, err := q.db.Query(ctx, getBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
@@ -347,14 +340,12 @@ const getBalancesByRuneId = `-- name: GetBalancesByRuneId :many
WITH balances AS (
SELECT DISTINCT ON (pkscript) pkscript, block_height, rune_id, amount FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC
)
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0 ORDER BY amount DESC, pkscript LIMIT $3 OFFSET $4
SELECT pkscript, block_height, rune_id, amount FROM balances WHERE amount > 0
`
type GetBalancesByRuneIdParams struct {
RuneID string
BlockHeight int32
Limit int32
Offset int32
}
type GetBalancesByRuneIdRow struct {
@@ -365,12 +356,7 @@ type GetBalancesByRuneIdRow struct {
}
func (q *Queries) GetBalancesByRuneId(ctx context.Context, arg GetBalancesByRuneIdParams) ([]GetBalancesByRuneIdRow, error) {
rows, err := q.db.Query(ctx, getBalancesByRuneId,
arg.RuneID,
arg.BlockHeight,
arg.Limit,
arg.Offset,
)
rows, err := q.db.Query(ctx, getBalancesByRuneId, arg.RuneID, arg.BlockHeight)
if err != nil {
return nil, err
}
@@ -645,106 +631,26 @@ func (q *Queries) GetRuneIdFromRune(ctx context.Context, rune string) (string, e
return rune_id, err
}
const getRuneTransaction = `-- name: GetRuneTransaction :one
SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE hash = $1 LIMIT 1
`
type GetRuneTransactionRow struct {
Hash string
BlockHeight int32
Index int32
Timestamp pgtype.Timestamp
Inputs []byte
Outputs []byte
Mints []byte
Burns []byte
RuneEtched bool
TxHash pgtype.Text
BlockHeight_2 pgtype.Int4
Etching pgtype.Bool
EtchingDivisibility pgtype.Int2
EtchingPremine pgtype.Numeric
EtchingRune pgtype.Text
EtchingSpacers pgtype.Int4
EtchingSymbol pgtype.Int4
EtchingTerms pgtype.Bool
EtchingTermsAmount pgtype.Numeric
EtchingTermsCap pgtype.Numeric
EtchingTermsHeightStart pgtype.Int4
EtchingTermsHeightEnd pgtype.Int4
EtchingTermsOffsetStart pgtype.Int4
EtchingTermsOffsetEnd pgtype.Int4
EtchingTurbo pgtype.Bool
Edicts []byte
Mint pgtype.Text
Pointer pgtype.Int4
Cenotaph pgtype.Bool
Flaws pgtype.Int4
}
func (q *Queries) GetRuneTransaction(ctx context.Context, hash string) (GetRuneTransactionRow, error) {
row := q.db.QueryRow(ctx, getRuneTransaction, hash)
var i GetRuneTransactionRow
err := row.Scan(
&i.Hash,
&i.BlockHeight,
&i.Index,
&i.Timestamp,
&i.Inputs,
&i.Outputs,
&i.Mints,
&i.Burns,
&i.RuneEtched,
&i.TxHash,
&i.BlockHeight_2,
&i.Etching,
&i.EtchingDivisibility,
&i.EtchingPremine,
&i.EtchingRune,
&i.EtchingSpacers,
&i.EtchingSymbol,
&i.EtchingTerms,
&i.EtchingTermsAmount,
&i.EtchingTermsCap,
&i.EtchingTermsHeightStart,
&i.EtchingTermsHeightEnd,
&i.EtchingTermsOffsetStart,
&i.EtchingTermsOffsetEnd,
&i.EtchingTurbo,
&i.Edicts,
&i.Mint,
&i.Pointer,
&i.Cenotaph,
&i.Flaws,
)
return i, err
}
const getRuneTransactions = `-- name: GetRuneTransactions :many
SELECT hash, runes_transactions.block_height, index, timestamp, inputs, outputs, mints, burns, rune_etched, tx_hash, runes_runestones.block_height, etching, etching_divisibility, etching_premine, etching_rune, etching_spacers, etching_symbol, etching_terms, etching_terms_amount, etching_terms_cap, etching_terms_height_start, etching_terms_height_end, etching_terms_offset_start, etching_terms_offset_end, etching_turbo, edicts, mint, pointer, cenotaph, flaws FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE (
$3::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
$1::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR runes_transactions.outputs @> $2::JSONB
OR runes_transactions.inputs @> $2::JSONB
) AND (
$3::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $4::JSONB
OR runes_transactions.inputs @> $4::JSONB
OR runes_transactions.inputs @> $4::JSONB
OR runes_transactions.mints ? $5
OR runes_transactions.burns ? $5
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $6 AND runes_transactions.index = $7)
) AND (
$5::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> $6::JSONB
OR runes_transactions.inputs @> $6::JSONB
OR runes_transactions.mints ? $7
OR runes_transactions.burns ? $7
OR (runes_transactions.rune_etched = TRUE AND runes_transactions.block_height = $8 AND runes_transactions.index = $9)
) AND (
$10 <= runes_transactions.block_height AND runes_transactions.block_height <= $11
$8::INT = 0 OR runes_transactions.block_height = $8::INT -- if @block_height > 0, apply block_height filter
)
ORDER BY runes_transactions.block_height DESC, runes_transactions.index DESC LIMIT $1 OFFSET $2
`
type GetRuneTransactionsParams struct {
Limit int32
Offset int32
FilterPkScript bool
PkScriptParam []byte
FilterRuneID bool
@@ -752,8 +658,7 @@ type GetRuneTransactionsParams struct {
RuneID []byte
RuneIDBlockHeight int32
RuneIDTxIndex int32
FromBlock int32
ToBlock int32
BlockHeight int32
}
type GetRuneTransactionsRow struct {
@@ -791,8 +696,6 @@ type GetRuneTransactionsRow struct {
func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactionsParams) ([]GetRuneTransactionsRow, error) {
rows, err := q.db.Query(ctx, getRuneTransactions,
arg.Limit,
arg.Offset,
arg.FilterPkScript,
arg.PkScriptParam,
arg.FilterRuneID,
@@ -800,8 +703,7 @@ func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactio
arg.RuneID,
arg.RuneIDBlockHeight,
arg.RuneIDTxIndex,
arg.FromBlock,
arg.ToBlock,
arg.BlockHeight,
)
if err != nil {
return nil, err
@@ -852,114 +754,32 @@ func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactio
return items, nil
}
const getRunesUTXOsByPkScript = `-- name: GetRunesUTXOsByPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
const getUnspentOutPointBalancesByPkScript = `-- name: GetUnspentOutPointBalancesByPkScript :many
SELECT rune_id, pkscript, tx_hash, tx_idx, amount, block_height, spent_height FROM runes_outpoint_balances WHERE pkscript = $1 AND block_height <= $2 AND (spent_height IS NULL OR spent_height > $2)
`
type GetRunesUTXOsByPkScriptParams struct {
Limit int32
Offset int32
type GetUnspentOutPointBalancesByPkScriptParams struct {
Pkscript string
BlockHeight int32
}
type GetRunesUTXOsByPkScriptRow struct {
TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByPkScript(ctx context.Context, arg GetRunesUTXOsByPkScriptParams) ([]GetRunesUTXOsByPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
)
func (q *Queries) GetUnspentOutPointBalancesByPkScript(ctx context.Context, arg GetUnspentOutPointBalancesByPkScriptParams) ([]RunesOutpointBalance, error) {
rows, err := q.db.Query(ctx, getUnspentOutPointBalancesByPkScript, arg.Pkscript, arg.BlockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunesUTXOsByPkScriptRow
var items []RunesOutpointBalance
for rows.Next() {
var i GetRunesUTXOsByPkScriptRow
var i RunesOutpointBalance
if err := rows.Scan(
&i.RuneID,
&i.Pkscript,
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.RuneIds,
&i.Amounts,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRunesUTXOsByRuneIdAndPkScript = `-- name: GetRunesUTXOsByRuneIdAndPkScript :many
SELECT tx_hash, tx_idx, max("pkscript") as pkscript, array_agg("rune_id") as rune_ids, array_agg("amount") as amounts
FROM runes_outpoint_balances
WHERE
pkscript = $3 AND
block_height <= $4 AND
(spent_height IS NULL OR spent_height > $4)
GROUP BY tx_hash, tx_idx
HAVING array_agg("rune_id") @> $5::text[]
ORDER BY tx_hash, tx_idx
LIMIT $1 OFFSET $2
`
type GetRunesUTXOsByRuneIdAndPkScriptParams struct {
Limit int32
Offset int32
Pkscript string
BlockHeight int32
RuneIds []string
}
type GetRunesUTXOsByRuneIdAndPkScriptRow struct {
TxHash string
TxIdx int32
Pkscript interface{}
RuneIds interface{}
Amounts interface{}
}
func (q *Queries) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, arg GetRunesUTXOsByRuneIdAndPkScriptParams) ([]GetRunesUTXOsByRuneIdAndPkScriptRow, error) {
rows, err := q.db.Query(ctx, getRunesUTXOsByRuneIdAndPkScript,
arg.Limit,
arg.Offset,
arg.Pkscript,
arg.BlockHeight,
arg.RuneIds,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunesUTXOsByRuneIdAndPkScriptRow
for rows.Next() {
var i GetRunesUTXOsByRuneIdAndPkScriptRow
if err := rows.Scan(
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.RuneIds,
&i.Amounts,
&i.Amount,
&i.BlockHeight,
&i.SpentHeight,
); err != nil {
return nil, err
}

View File

@@ -638,72 +638,6 @@ func mapIndexedBlockTypeToParams(src entity.IndexedBlock) (gen.CreateIndexedBloc
}, nil
}
func mapRunesUTXOModelToType(src gen.GetRunesUTXOsByPkScriptRow) (entity.RunesUTXO, error) {
pkScriptRaw, ok := src.Pkscript.(string)
if !ok {
return entity.RunesUTXO{}, errors.New("pkscript from database is not string")
}
pkScript, err := hex.DecodeString(pkScriptRaw)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse pkscript")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse tx hash")
}
runeIdsRaw, ok := src.RuneIds.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("src.RuneIds is not a slice")
}
runeIds := make([]string, 0, len(runeIdsRaw))
for i, raw := range runeIdsRaw {
runeId, ok := raw.(string)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.RuneIds[%d] is not a string", i)
}
runeIds = append(runeIds, runeId)
}
amountsRaw, ok := src.Amounts.([]interface{})
if !ok {
return entity.RunesUTXO{}, errors.New("amounts from database is not a slice")
}
amounts := make([]pgtype.Numeric, 0, len(amountsRaw))
for i, raw := range amountsRaw {
amount, ok := raw.(pgtype.Numeric)
if !ok {
return entity.RunesUTXO{}, errors.Errorf("src.Amounts[%d] is not pgtype.Numeric", i)
}
amounts = append(amounts, amount)
}
if len(runeIds) != len(amounts) {
return entity.RunesUTXO{}, errors.New("rune ids and amounts have different lengths")
}
runesBalances := make([]entity.RunesUTXOBalance, 0, len(runeIds))
for i := range runeIds {
runeId, err := runes.NewRuneIdFromString(runeIds[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse rune id")
}
amount, err := uint128FromNumeric(amounts[i])
if err != nil {
return entity.RunesUTXO{}, errors.Wrap(err, "failed to parse amount")
}
runesBalances = append(runesBalances, entity.RunesUTXOBalance{
RuneId: runeId,
Amount: lo.FromPtr(amount),
})
}
return entity.RunesUTXO{
PkScript: pkScript,
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.TxIdx),
},
RuneBalances: runesBalances,
}, nil
}
func mapOutPointBalanceModelToType(src gen.RunesOutpointBalance) (entity.OutPointBalance, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@@ -63,18 +62,7 @@ func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64)
return indexedBlock, nil
}
const maxRuneTransactionsLimit = 10000 // temporary limit to prevent large queries from overwhelming the database
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
if limit == -1 {
limit = maxRuneTransactionsLimit
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
if limit > maxRuneTransactionsLimit {
return nil, errors.Wrapf(errs.InvalidArgument, "limit cannot exceed %d", maxRuneTransactionsLimit)
}
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, height uint64) ([]*entity.RuneTransaction, error) {
pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript)))
runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String()))
rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{
@@ -87,11 +75,7 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
RuneIDBlockHeight: int32(runeId.BlockHeight),
RuneIDTxIndex: int32(runeId.TxIndex),
FromBlock: int32(fromBlock),
ToBlock: int32(toBlock),
Limit: limit,
Offset: offset,
BlockHeight: int32(height),
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
@@ -120,33 +104,6 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
return runeTxs, nil
}
func (r *Repository) GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error) {
row, err := r.queries.GetRuneTransaction(ctx, txHash.String())
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
runeTxModel, runestoneModel, err := extractModelRuneTxAndRunestone(gen.GetRuneTransactionsRow(row))
if err != nil {
return nil, errors.Wrap(err, "failed to extract rune transaction and runestone from row")
}
runeTx, err := mapRuneTransactionModelToType(runeTxModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse rune transaction model")
}
if runestoneModel != nil {
runestone, err := mapRunestoneModelToType(*runestoneModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse runestone model")
}
runeTx.Runestone = &runestone
}
return &runeTx, nil
}
func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error) {
balances, err := r.queries.GetOutPointBalancesAtOutPoint(ctx, gen.GetOutPointBalancesAtOutPointParams{
TxHash: outPoint.Hash.String(),
@@ -167,59 +124,22 @@ func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wi
return result, nil
}
func (r *Repository) GetRunesUTXOsByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByPkScript(ctx, gen.GetRunesUTXOsByPkScriptParams{
func (r *Repository) GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error) {
balances, err := r.queries.GetUnspentOutPointBalancesByPkScript(ctx, gen.GetUnspentOutPointBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.RunesUTXO, 0, len(rows))
for _, row := range rows {
utxo, err := mapRunesUTXOModelToType(row)
result := make([]*entity.OutPointBalance, 0, len(balances))
for _, balanceModel := range balances {
balance, err := mapOutPointBalanceModelToType(balanceModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse row model")
return nil, errors.Wrap(err, "failed to parse balance model")
}
result = append(result, &utxo)
}
return result, nil
}
func (r *Repository) GetRunesUTXOsByRuneIdAndPkScript(ctx context.Context, runeId runes.RuneId, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.RunesUTXO, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
rows, err := r.queries.GetRunesUTXOsByRuneIdAndPkScript(ctx, gen.GetRunesUTXOsByRuneIdAndPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
RuneIds: []string{runeId.String()},
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.RunesUTXO, 0, len(rows))
for _, row := range rows {
utxo, err := mapRunesUTXOModelToType(gen.GetRunesUTXOsByPkScriptRow(row))
if err != nil {
return nil, errors.Wrap(err, "failed to parse row")
}
result = append(result, &utxo)
result = append(result, &balance)
}
return result, nil
}
@@ -324,46 +244,30 @@ func (r *Repository) CountRuneEntries(ctx context.Context) (uint64, error) {
return uint64(count), nil
}
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
func (r *Repository) GetBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) (map[runes.RuneId]*entity.Balance, error) {
balances, err := r.queries.GetBalancesByPkScript(ctx, gen.GetBalancesByPkScriptParams{
Pkscript: hex.EncodeToString(pkScript),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make([]*entity.Balance, 0, len(balances))
result := make(map[runes.RuneId]*entity.Balance, len(balances))
for _, balanceModel := range balances {
balance, err := mapBalanceModelToType(gen.RunesBalance(balanceModel))
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance model")
}
result = append(result, balance)
result[balance.RuneId] = balance
}
return result, nil
}
func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64, limit int32, offset int32) ([]*entity.Balance, error) {
if limit == -1 {
limit = math.MaxInt32
}
if limit < 0 {
return nil, errors.Wrap(errs.InvalidArgument, "limit must be -1 or non-negative")
}
func (r *Repository) GetBalancesByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) ([]*entity.Balance, error) {
balances, err := r.queries.GetBalancesByRuneId(ctx, gen.GetBalancesByRuneIdParams{
RuneID: runeId.String(),
BlockHeight: int32(blockHeight),
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, errors.Wrap(err, "error during query")

View File

@@ -29,10 +29,6 @@ var ErrInvalidBase26 = errors.New("invalid base-26 character: must be in the ran
func NewRuneFromString(value string) (Rune, error) {
n := uint128.From64(0)
for i, char := range value {
// skip spacers
if char == '.' || char == '•' {
continue
}
if i > 0 {
n = n.Add(uint128.From64(1))
}

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,8 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/runes"
)
// Use limit = -1 as no limit.
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64, limit int32, offset int32) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock, limit, offset)
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, height uint64) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetTransactionsByHeight")
}

View File

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

View File

@@ -4,11 +4,9 @@ import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/gaze-network/indexer-network/core/types"
)
type Contract interface {
GetRawTransactionAndHeightByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, int64, error)
GetRawTransactionByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, error)
GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error)
}

View File

@@ -1,212 +0,0 @@
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"
)
const (
// MaxSupportedPkScriptSize is the maximum supported size of a pkScript.
MaxSupportedPkScriptSize = 40
)
// 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 [MaxSupportedPkScriptSize]byte
scriptPubKeySize int
}
// 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")
}
fixedPkScript := [MaxSupportedPkScriptSize]byte{}
copy(fixedPkScript[:], scriptPubkey)
return Address{
decoded: decoded,
net: net,
encoded: decoded.EncodeAddress(),
encodedType: addrType,
scriptPubKey: fixedPkScript,
scriptPubKeySize: len(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[:a.scriptPubKeySize]
}
// 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")
}
}

View File

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

View File

@@ -1,449 +0,0 @@
package btcutils_test
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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)
})
}
func TestAddressPkScript(t *testing.T) {
anyErr := 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: anyErr,
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(spec.Address, func(t *testing.T) {
addr, err := btcutils.SafeNewAddress(spec.Address, spec.DefaultNet)
if spec.ExpectedError != nil {
if errors.Is(spec.ExpectedError, anyErr) {
require.Error(t, err)
} else {
require.ErrorIs(t, err, spec.ExpectedError)
}
return
}
require.NoError(t, err)
assert.Equal(t, spec.ExpectedPkScript, hex.EncodeToString(addr.ScriptPubKey()))
})
}
}

View File

@@ -1,58 +0,0 @@
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
)
// IsSupportType returns true if the given tx/address type is supported.
func IsSupportType(t txscript.ScriptClass) bool {
_, ok := supportedTypes[t]
return ok
}
var supportedTypes = map[txscript.ScriptClass]struct{}{
txscript.WitnessV0PubKeyHashTy: {},
txscript.WitnessV1TaprootTy: {},
txscript.ScriptHashTy: {},
txscript.PubKeyHashTy: {},
txscript.WitnessV0ScriptHashTy: {},
}

View File

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

View File

@@ -1,69 +0,0 @@
package btcutils
import (
"github.com/Cleverse/go-utilities/utils"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
// 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) {
if len(pkScript) == 0 {
return Address{}, errors.New("empty pkScript")
}
if pkScript[0] == txscript.OP_RETURN {
return Address{}, errors.Wrap(errs.NotSupported, "OP_RETURN script")
}
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 !IsSupportType(addrType) {
return Address{}, errors.Wrapf(errs.NotSupported, "unsupported pkscript type %s", addrType)
}
if len(addrs) == 0 {
return Address{}, errors.New("can't extract address from pkScript")
}
fixedPkScript := [MaxSupportedPkScriptSize]byte{}
copy(fixedPkScript[:], pkScript)
return Address{
decoded: addrs[0],
net: net,
encoded: addrs[0].EncodeAddress(),
encodedType: addrType,
scriptPubKey: fixedPkScript,
scriptPubKeySize: len(pkScript),
}, nil
}

View File

@@ -1,217 +0,0 @@
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,
},
{
PubkeyScript: "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70",
ExpectedError: nil,
ExpectedAddressType: btcutils.AddressP2WSH,
},
{
PubkeyScript: "6a5d0614c0a2331441",
ExpectedError: nil,
ExpectedAddressType: txscript.NonStandardTy,
},
}
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)
}
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,28 +10,23 @@ import (
"github.com/gofiber/fiber/v2"
)
// New setup error handler middleware
func New() fiber.Handler {
return func(ctx *fiber.Ctx) error {
err := ctx.Next()
if err == nil {
return nil
}
func NewHTTPErrorHandler() func(ctx *fiber.Ctx, err error) error {
return func(ctx *fiber.Ctx, err error) error {
if e := new(errs.PublicError); errors.As(err, &e) {
return errors.WithStack(ctx.Status(http.StatusBadRequest).JSON(fiber.Map{
return errors.WithStack(ctx.Status(http.StatusBadRequest).JSON(map[string]any{
"error": e.Message(),
}))
}
if e := new(fiber.Error); errors.As(err, &e) {
return errors.WithStack(ctx.Status(e.Code).JSON(fiber.Map{
"error": e.Error(),
}))
return errors.WithStack(ctx.Status(e.Code).SendString(e.Error()))
}
logger.ErrorContext(ctx.UserContext(), "Something went wrong, api error",
slogx.String("event", "api_error"),
logger.ErrorContext(ctx.UserContext(), "Something went wrong, unhandled api error",
slogx.String("event", "api_unhandled_error"),
slogx.Error(err),
)
return errors.WithStack(ctx.Status(http.StatusInternalServerError).JSON(fiber.Map{
return errors.WithStack(ctx.Status(http.StatusInternalServerError).JSON(map[string]any{
"error": "Internal Server Error",
}))
}

View File

@@ -5,11 +5,12 @@ import (
"encoding/json"
"log/slog"
"net/url"
"path"
"strings"
"time"
"github.com/Cleverse/go-utilities/utils"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/valyala/fasthttp"
)
@@ -23,14 +24,13 @@ type Config struct {
}
type Client struct {
baseURL *url.URL
baseURL string
Config
}
func New(baseURL string, config ...Config) (*Client, error) {
parsedBaseURL, err := url.Parse(baseURL)
if err != nil {
return nil, errors.Wrap(err, "can't parse base url")
if _, err := url.Parse(baseURL); err != nil {
return nil, errors.Join(errs.InvalidArgument, errors.Wrap(err, "can't parse base url"))
}
var cf Config
if len(config) > 0 {
@@ -40,7 +40,7 @@ func New(baseURL string, config ...Config) (*Client, error) {
cf.Headers = make(map[string]string)
}
return &Client{
baseURL: parsedBaseURL,
baseURL: baseURL,
Config: cf,
}, nil
}
@@ -60,22 +60,11 @@ type HttpResponse struct {
}
func (r *HttpResponse) UnmarshalBody(out any) error {
body, err := r.BodyUncompressed()
err := json.Unmarshal(r.Body(), out)
if err != nil {
return errors.Wrapf(err, "can't uncompress body from %v", r.URL)
}
contentType := strings.ToLower(string(r.Header.ContentType()))
switch {
case strings.Contains(contentType, "application/json"):
if err := json.Unmarshal(body, out); err != nil {
return errors.Wrapf(err, "can't unmarshal json body from %s, %q", r.URL, string(body))
}
return nil
case strings.Contains(contentType, "text/plain"):
return errors.Errorf("can't unmarshal plain text %q", string(body))
default:
return errors.Errorf("unsupported content type: %s, contents: %v", r.Header.ContentType(), string(r.Body()))
return errors.Wrapf(err, "can't unmarshal json body from %v, %v", r.URL, string(r.Body()))
}
return nil
}
func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpResponse, error) {
@@ -88,18 +77,9 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
for k, v := range reqOptions.Header {
req.Header.Set(k, v)
}
parsedUrl := h.BaseURL()
parsedUrl.Path = path.Join(parsedUrl.Path, reqOptions.path)
// Because path.Join cleans the joined path. If path ends with /, append "/" to parsedUrl.Path
if strings.HasSuffix(reqOptions.path, "/") && !strings.HasSuffix(parsedUrl.Path, "/") {
parsedUrl.Path += "/"
}
baseQuery := parsedUrl.Query()
for k, v := range reqOptions.Query {
baseQuery[k] = v
}
parsedUrl.RawQuery = baseQuery.Encode()
parsedUrl := utils.Must(url.Parse(h.baseURL)) // checked in httpclient.New
parsedUrl.Path = reqOptions.path
parsedUrl.RawQuery = reqOptions.Query.Encode()
// remove %20 from url (empty space)
url := strings.TrimSuffix(parsedUrl.String(), "%20")
@@ -131,7 +111,6 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
logger = logger.With(
slog.Int("status_code", resp.StatusCode()),
slog.String("resp_content_type", string(resp.Header.ContentType())),
slog.String("resp_content_encoding", string(resp.Header.ContentEncoding())),
slog.Int("resp_content_length", len(resp.Body())),
)
}
@@ -155,12 +134,6 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
return &httpResponse, nil
}
// BaseURL returns the cloned base URL of the client.
func (h *Client) BaseURL() *url.URL {
u := *h.baseURL
return &u
}
func (h *Client) Do(ctx context.Context, method, path string, reqOptions RequestOptions) (*HttpResponse, error) {
reqOptions.path = path
reqOptions.method = method

View File

@@ -119,10 +119,10 @@ type Config struct {
// - Text (default)
// - JSON
// - GCP: Output format for Stackdriver Logging/Cloud Logging or others GCP services.
Output string `mapstructure:"output" env:"OUTPUT" envDefault:"text"`
Output string `mapstructure:"output"`
// Debug is enabled logger level debug. (default: false)
Debug bool `mapstructure:"debug" env:"DEBUG" envDefault:"false"`
Debug bool `mapstructure:"debug"`
}
var (

View File

@@ -13,13 +13,10 @@ import (
)
type Config struct {
AllRequestHeaders bool `env:"REQUEST_HEADER" envDefault:"false" mapstructure:"request_header"` // Log all request headers
AllResponseHeaders bool `env:"RESPONSE_HEADER" envDefault:"false" mapstructure:"response_header"` // Log all response headers
AllRequestQueries bool `env:"REQUEST_QUERY" envDefault:"false" mapstructure:"request_query"` // Log all request queries
Disable bool `env:"DISABLE" envDefault:"false" mapstructure:"disable"` // Disable logger level `INFO`
HiddenRequestHeaders []string `env:"HIDDEN_REQUEST_HEADERS" mapstructure:"hidden_request_headers"` // Hide specific headers from log
WithRequestHeaders []string `env:"WITH_REQUEST_HEADERS" mapstructure:"with_request_headers"` // Add specific headers to log (higher priority than `HiddenRequestHeaders`)
With map[string]interface{} `env:"WITH" mapstructure:"with"` // Additional fields to log
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
@@ -28,10 +25,6 @@ func New(config Config) fiber.Handler {
for _, header := range config.HiddenRequestHeaders {
hiddenRequestHeaders[strings.TrimSpace(strings.ToLower(header))] = struct{}{}
}
withRequestHeaders := make(map[string]struct{}, len(config.WithRequestHeaders))
for _, header := range config.WithRequestHeaders {
withRequestHeaders[strings.TrimSpace(strings.ToLower(header))] = struct{}{}
}
return func(c *fiber.Ctx) error {
start := time.Now()
@@ -48,11 +41,6 @@ func New(config Config) fiber.Handler {
slog.String("latencyHuman", latency.String()),
}
// add `with` fields
for k, v := range config.With {
baseAttrs = append(baseAttrs, slog.Any(k, v))
}
// prep request attributes
requestAttributes := []slog.Attr{
slog.Time("time", start),
@@ -65,7 +53,6 @@ func New(config Config) fiber.Handler {
slog.Any("x-forwarded-for", c.IPs()),
slog.String("user-agent", string(c.Context().UserAgent())),
slog.Any("params", c.AllParams()),
slog.Any("query", c.Queries()),
slog.Int("length", len((c.Body()))),
}
@@ -76,64 +63,23 @@ func New(config Config) fiber.Handler {
slog.Int("length", len(c.Response().Body())),
}
// request queries
if config.AllRequestQueries {
args := c.Request().URI().QueryArgs()
logAttrs := make([]any, 0, args.Len())
args.VisitAll(func(k, v []byte) {
logAttrs = append(logAttrs, slog.Any(string(k), string(v)))
})
requestAttributes = append(requestAttributes, slog.Group("queries", logAttrs...))
// request query
if config.WithRequestQuery {
requestAttributes = append(requestAttributes, slog.String("query", string(c.Request().URI().QueryString())))
}
// request headers
if config.AllRequestHeaders || len(config.WithRequestHeaders) > 0 {
if config.WithRequestHeader {
kv := []any{}
for k, v := range c.GetReqHeaders() {
h := strings.ToLower(k)
// add headers for WithRequestHeaders
if _, found := withRequestHeaders[h]; found {
goto add
}
// skip hidden headers
if _, found := hiddenRequestHeaders[h]; found {
continue
}
// skip if not AllRequestHeaders
if !config.AllRequestHeaders {
continue
}
add:
val := any(v)
if len(v) == 1 {
val = v[0]
}
kv = append(kv, slog.Any(k, val))
}
requestAttributes = append(requestAttributes, slog.Group("headers", kv...))
}
if config.AllResponseHeaders {
kv := []any{}
for k, v := range c.GetRespHeaders() {
// skip hidden headers
if _, found := hiddenRequestHeaders[strings.ToLower(k)]; found {
continue
}
val := any(v)
if len(v) == 1 {
val = v[0]
}
kv = append(kv, slog.Any(k, val))
kv = append(kv, slog.Any(k, v))
}
responseAttributes = append(responseAttributes, slog.Group("headers", kv...))
requestAttributes = append(requestAttributes, slog.Group("header", kv...))
}
level := slog.LevelInfo

View File

@@ -25,3 +25,4 @@ sql:
package: "gen"
out: "./modules/nodesale/repository/postgres/gen"
sql_package: "pgx/v5"
emit_interface: true