Compare commits

..

425 Commits

Author SHA1 Message Date
PanJ
4b2da298ba read private key from file 2024-05-21 15:18:55 +07:00
Panjamapong Sermsawatsri
bd9acb8841 feat: fix reporting errors 2024-05-15 17:09:48 +07:00
PanJ
58761b1ef5 update reporting api 2024-05-15 15:33:59 +07:00
Panjamapong Sermsawatsri
85fdf79a00 fix: wif file name to add mainnet 2024-05-08 10:35:21 +07:00
Panjamapong Sermsawatsri
e0e2934160 feat: add wif generation 2024-05-08 10:28:47 +07:00
Panjamapong Sermsawatsri
d56c58334a feat: add new reporting client 2024-04-26 16:20:24 +07:00
Panjamapong Sermsawatsri
be316442ea feat: generate key command and crypto pkg 2024-04-23 16:44:16 +07:00
Gaze
185d26c651 feat: add more index 2024-04-23 13:28:29 +07:00
Gaze
a0b15f452d fix: wrong condition 2024-04-23 00:49:19 +07:00
Gaze
8a48fc1321 feat: add decimals in get transactions 2024-04-23 00:06:26 +07:00
Gaze
9a3c6083c2 fix(runes): handling latest block not found
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 19:21:39 +07:00
Gaze
05b044e935 Merge branch 'main' into feature/bitcoin-indexer 2024-04-22 16:50:55 +07:00
Gaze
f549929f65 Merge branch 'main' into feature/bitcoin-indexer 2024-04-22 16:49:46 +07:00
Gaze
286e21e856 Merge branch 'main' into feature/bitcoin-indexer 2024-04-22 16:40:14 +07:00
Gaze
913a0bb9e8 Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-22 15:58:52 +07:00
Gaze
2da45e4294 fix(btc): fix with modified same row twice
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 15:58:41 +07:00
Gaze
b606bca814 feat: add starting height for runes mainnet 2024-04-22 14:56:05 +07:00
Gaze
bf95874b26 fix: increase max reorg limit 2024-04-22 14:39:27 +07:00
gazenw
825a79c539 Merge pull request #3 from gaze-network/feat/reporting-client
Implement reporting client to report runes blocks
2024-04-22 14:14:21 +07:00
Gaze
732df9fe4e fix: remove module enum 2024-04-22 13:16:55 +07:00
Gaze
0e583b1704 fix(indexer): fix subscription closed before process when success fetch
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 05:52:28 +07:00
Gaze
1edc5b0981 refactor(btc): readable code
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 05:32:44 +07:00
Gaze
0bb56e6ef3 fix: annoying error when unsubscribe fetcher
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 03:24:35 +07:00
Gaze
877d55fcff fix(btc): txout aren't revert if it's have to revert spent
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 03:00:44 +07:00
Gaze
fcdc54d360 feat(btc): use logger warn
Co-authored-by: Gaze <dev@gaze.network>
2024-04-22 02:46:57 +07:00
Gaze
4a1819c3f0 Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-21 02:18:29 +07:00
Gaze
d07ad3c0e9 feat(runes): add latest block api
Co-authored-by: Gaze <dev@gaze.network>
2024-04-21 02:18:16 +07:00
Gaze
e9cde98a9b feat: implement report node 2024-04-21 02:04:48 +07:00
Gaze
83557ce1cf feat: implement reporting client to report runes blocks 2024-04-21 00:30:39 +07:00
Gaze
050ffaf5ef fix: wrong index type 2024-04-21 00:26:12 +07:00
Gaze
df063c73f0 feat(reorg): add reorg detail
Co-authored-by: Gaze <dev@gaze.network>
2024-04-20 19:51:07 +07:00
Gaze
60ac0c3580 fix: ignore zero burn amount 2024-04-20 18:36:13 +07:00
Gaze
22691fc781 fix: don't add zero mint to unallocated 2024-04-20 18:35:25 +07:00
Gaze
8f4621b2ad fix: create index 2024-04-20 16:34:25 +07:00
Gaze
a02f799924 feat: add flag --api-only to run cmd 2024-04-20 13:49:40 +07:00
Gaze
b9b7e85570 fix: main.go 2024-04-20 01:50:19 +07:00
Gaze
9915251f2a fix: remove unused funcs 2024-04-19 00:46:31 +07:00
Gaze
be1a9ff896 fix: revert data sequentially 2024-04-19 00:45:52 +07:00
Gaze
83df900d7f fix: empty etched at 2024-04-18 23:55:59 +07:00
Gaze
7de6357ff3 chore: add performance logger for debyug
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 22:26:50 +07:00
Gaze
e255c765c7 chore: add performance logging
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 22:24:12 +07:00
Gaze
81fb883bbb perf: add automaxprocs
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 22:09:45 +07:00
Gaze
bc40a462c7 ci: add arg run
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 21:54:21 +07:00
Gaze
b05ca3f352 ci: create Dockerfile
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 21:38:38 +07:00
Gaze
fc66b3b4fd Merge branch 'feat/runes-module' into feature/bitcoin-indexer 2024-04-18 20:46:43 +07:00
Gaze
1abf9aa345 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-18 19:21:26 +07:00
Gaze
e2089a68d8 feat: sort balances result by amount 2024-04-18 18:28:44 +07:00
Gaze
9eb7703e8d feat: implement get balances by address batch 2024-04-18 18:26:14 +07:00
Gaze
f6e2ff2029 fix: spend outpoint bug 2024-04-18 17:59:06 +07:00
Gaze
1cd61b8fa1 feat: implement get utxos by address api 2024-04-18 17:53:29 +07:00
Gaze
df326a7aab feat: add pkscript to outpoint balance 2024-04-18 17:19:00 +07:00
Gaze
6d77184751 fix: rename type 2024-04-18 16:33:45 +07:00
Gaze
a136229e31 fix: return a new repository when beginning a new tx 2024-04-18 16:10:11 +07:00
Gaze
efbed0766c feat: implement get holders 2024-04-18 02:13:20 +07:00
Gaze
a28e062fa5 feat: add holders count in token info 2024-04-18 01:37:22 +07:00
Gaze
b307b68c70 feat: implement get token info 2024-04-18 01:27:49 +07:00
Gaze
ee7f2f7a4d feat: add runeEtched in get transactions 2024-04-18 00:26:50 +07:00
Gaze
9caf31d3ab fix: missing field in create 2024-04-18 00:17:27 +07:00
Gaze
8f83ec55b2 feat: add rune_etched column in rune transaction 2024-04-18 00:06:54 +07:00
Gaze
0fa6d77b4d feat(btc): update chuunk size
Co-authored-by: Gaze <dev@gaze.network>
2024-04-18 00:03:02 +07:00
Gaze
3f96a762ef feat: fix interface 2024-04-17 23:55:09 +07:00
Gaze
c7f7d276e6 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-17 22:59:33 +07:00
Gaze
07b1f864c1 refactor: sorting code
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 22:51:25 +07:00
Gaze
2f9a2d3141 Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-17 22:50:30 +07:00
Gaze
8498a06b4d feat(btc): add verify indexer states
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 22:50:20 +07:00
Gaze
0c7399dd4c fix: wrong condition for non-OP_RETURN output 2024-04-17 22:42:56 +07:00
Gaze
1ce88ddcb4 feat: implement get transactions 2024-04-17 22:40:01 +07:00
Gaze
182719a0bd Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 22:39:50 +07:00
Gaze
3aa25fadab feat: add chainparam 2024-04-17 22:39:37 +07:00
Gaze
d035003d43 feat(btc): add common.ZeroHash
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 22:25:51 +07:00
Gaze
e2d8d906e1 feat(btc): get indexed block impl
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 22:07:40 +07:00
Gaze
5bee86e996 feat(btc): upgrade data type for idx
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 22:01:03 +07:00
Gaze
ba3333ff9d fix(btc): upgrade data type for numbers
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 21:54:46 +07:00
Gaze
d1792d0fe1 fix(btc): use int64 to store sequence
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 21:09:02 +07:00
Gaze
39949cd443 doc: add note
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 21:01:22 +07:00
Gaze
e186ad895d feat(btc): add default current block
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 20:55:23 +07:00
Gaze
8e4780d4dc feat(btc): update index
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 20:41:38 +07:00
Gaze
10a82bde76 feat: rename id field to runeId in rune transaction 2024-04-17 20:29:34 +07:00
Gaze
e32f0d7459 refactor: move runes out of internal 2024-04-17 20:22:11 +07:00
Gaze
e6fc590f58 feat(btc): change rollback style
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 20:19:33 +07:00
Gaze
8c3bdbfe05 feat(btc): nullable pkscript
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 20:13:50 +07:00
Gaze
123f541c54 Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-17 20:13:24 +07:00
Gaze
2d6736735a feat: bump cstream version
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 20:11:39 +07:00
Gaze
83c02c6ebd Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 20:11:18 +07:00
Gaze
624577f42c Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-17 20:11:13 +07:00
Gaze
0cf7cecaf8 fix: change WithPublicMessage to be prefix 2024-04-17 20:11:11 +07:00
Gaze
b6764aa4ac ci: ignore RollBack error 2024-04-17 20:11:01 +07:00
Gaze
94a40ef9dd fix: off by one confirmation 2024-04-17 19:46:23 +07:00
Gaze
723cc3739e fix: first empty state error
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 19:30:26 +07:00
Gaze
4c244e3222 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 18:57:10 +07:00
Gaze
d6a30f73fd fix: error handler 2024-04-17 18:57:05 +07:00
Gaze
c8281a772f fix: mount runes api 2024-04-17 18:56:51 +07:00
Gaze
47e2f74791 Merge branch 'feature/bitcoin-indexer' into feat/runes-module
# Conflicts:
#	cmd/cmd_run.go
#	go.mod
2024-04-17 18:46:13 +07:00
Gaze
7678e47b2e feat: implement http server 2024-04-17 18:45:16 +07:00
Gaze
27288b3b0a feat: refactor runes api 2024-04-17 18:39:11 +07:00
Gaze
a2598e4ef8 fix: invalid indexer flow
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 18:26:07 +07:00
Gaze
f3363313fb fix: invalid pgx version
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 18:25:55 +07:00
Gaze
c57751f7f4 feat(btc): add err notfound
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 18:19:53 +07:00
Gaze
7c41302873 feat: support postgresql db
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 18:08:21 +07:00
Gaze
27c835cee4 feat(logger): remove error verbose
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:52:06 +07:00
Gaze
a2294d2c58 doc: add todo
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:50:42 +07:00
Gaze
fa581bbabc fix(btc): remove uniqte index for hash
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:46:49 +07:00
Gaze
4a1aa6372d fix(btc): invalid table name
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:44:13 +07:00
Gaze
accae9e27c Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-17 17:40:15 +07:00
Gaze
287ee65be7 feat(btc): witness datamodel parsing
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:39:39 +07:00
Gaze
996676e76e feat(btc): add witness utils
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 17:34:34 +07:00
Gaze
4e350c0e3d fix: duplicate rune entry number 2024-04-17 17:24:34 +07:00
Gaze
eb732722a6 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 17:18:07 +07:00
Gaze
46159c2089 chore: add log 2024-04-17 17:18:02 +07:00
Gaze
329f140056 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 17:16:59 +07:00
Gaze
1c597f740c fix: move stop() inside goroutine 2024-04-17 17:16:52 +07:00
Gaze
82ece573fd fix: stop main if runes indexer failed 2024-04-17 16:19:16 +07:00
Gaze
6de6bd0802 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 16:19:00 +07:00
Gaze
56e3a780cd fix: stop main if bitcoin indexer failed 2024-04-17 16:18:55 +07:00
Gaze
d0ef6c1778 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 16:15:51 +07:00
Gaze
d316950098 fix: reduce chunk size 2024-04-17 16:15:46 +07:00
Gaze
38517deaae feat: integrate bitcoin client db to main 2024-04-17 16:13:42 +07:00
Gaze
21142a6c65 Merge branch 'feature/bitcoin-indexer' into feat/runes-module
# Conflicts:
#	core/datasources/bitcoin_node.go
2024-04-17 16:04:56 +07:00
Gaze
9ea39a72a5 fix: fix get transaction by hash 2024-04-17 16:04:36 +07:00
Gaze
1324e48b61 fix: rename func 2024-04-17 16:03:14 +07:00
Gaze
a5f72bd109 fix: rename func 2024-04-17 16:02:18 +07:00
Gaze
3d28444b87 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 16:01:19 +07:00
Gaze
0dc0e8ce28 fix: rename func 2024-04-17 16:01:13 +07:00
Gaze
77513776d3 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 15:58:39 +07:00
Gaze
fb87ee9629 feat: implement get tx by hash 2024-04-17 15:58:33 +07:00
Gaze
3ad904f940 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-17 14:28:41 +07:00
Gaze
51596c34ec feat(btc): add todo notes
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:47:54 +07:00
Gaze
080af7dc93 feat(btc): add get block header from db for reorg handling
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:44:44 +07:00
Gaze
323586fe43 feat(btc): update fetch async for db datasource
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:41:25 +07:00
Gaze
0b31dd8f95 fix(btc): return nil if errors
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:41:12 +07:00
Gaze
06d8a70a69 feat(btc): update dg
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:36:20 +07:00
Gaze
b22a026314 feat(btc): implement repo for get blocks
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 06:35:54 +07:00
Gaze
60e755a5b3 feat(btc): update queries
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:54:06 +07:00
Gaze
4fe523ba28 feat(btc): add pg queries for datasource
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:50:55 +07:00
Gaze
fba2e723a7 wip(btc): imple prepare range func
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:40:06 +07:00
Gaze
f632cd0b16 doc(btc): add note
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:38:03 +07:00
Gaze
76ca69cb6c feat(btc): add btc_database from datasource
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:36:55 +07:00
Gaze
abd7d02365 Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-17 05:28:56 +07:00
Gaze
4f651341c2 refactor: create subscription utils tools
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 05:28:42 +07:00
Gaze
baa5f8b438 fix: get the rest of transaction data in GetTransaction 2024-04-17 03:03:22 +07:00
Gaze
dc4ad9bc9b fix: zero len destination case 2024-04-17 01:38:16 +07:00
Gaze
e7ce29f9d4 fix: break if input utxo is not P2TR 2024-04-17 01:21:40 +07:00
Gaze
5ec2019674 feat: add custom opcode check for datapush 2024-04-17 01:17:08 +07:00
Gaze
46306d3927 refactor: move api and usecase outside of internal 2024-04-17 00:46:12 +07:00
Gaze
4396244937 fix: use errs in api 2024-04-17 00:34:13 +07:00
Gaze
e333e47ce4 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-17 00:33:38 +07:00
Gaze
faed162ca8 feat: implement public errors 2024-04-17 00:33:17 +07:00
Gaze
a008c20fb2 feat(btc): add revert data to processor
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 00:20:26 +07:00
Gaze
495386e575 feat(btc): add revert data query
Co-authored-by: Gaze <dev@gaze.network>
2024-04-17 00:16:39 +07:00
Gaze
8558cb8401 fix: commit every block 2024-04-16 23:44:28 +07:00
Gaze
b929fbed58 fix: migrated runes indexer integration to new cli 2024-04-16 23:38:15 +07:00
Gaze
f4508b6e80 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-16 23:35:55 +07:00
Gaze
733c04fce9 fix: rename postgres config key 2024-04-16 23:35:39 +07:00
Gaze
88ffbccc8d fix: interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 23:34:35 +07:00
Gaze
39f7502473 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-16 22:33:27 +07:00
Gaze
5c009aee5e feat(btc): reorg handling
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 06:07:42 +07:00
Gaze
4c925c6071 feat(btc): add get block header to datasource interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 05:45:36 +07:00
Gaze
5ff6366c51 chore(btc): update example config
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 05:19:38 +07:00
Gaze
15e7741a24 refactor(cli): separate protocol config and cli flag
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 05:17:28 +07:00
Gaze
68afc7d3fe feat: finish runestone tests 2024-04-16 02:11:00 +07:00
Gaze
73deab1f72 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module
# Conflicts:
#	cmd/main.go
2024-04-16 00:53:05 +07:00
Gaze
991f91f469 feat: add overflow err 2024-04-16 00:46:31 +07:00
Gaze
846aadc131 feat: more tests 2024-04-16 00:46:20 +07:00
Gaze
504458994a feat: add main.go
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:36:27 +07:00
Gaze
ec5bdb0331 refactor(cli): rename files
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:31:47 +07:00
Gaze
05fedfb03a Merge branch 'feature/cli-cmd' into feature/bitcoin-indexer 2024-04-16 00:28:07 +07:00
Gaze
acf2aa498b doc(cli): add refactor plan
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:27:41 +07:00
Gaze
e0e7511864 feat(cli): add version command
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:27:31 +07:00
Gaze
f742dcde84 fix: get data from cache in processor first, then dg 2024-04-16 00:15:06 +07:00
Gaze
d0dfff9dc0 fix: remove temp code
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:08:20 +07:00
Gaze
f92dbbbd78 feat(cli): support db selection
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:07:03 +07:00
Gaze
d2b4a614e5 refactor(cli): flags for each module
Co-authored-by: Gaze <dev@gaze.network>
2024-04-16 00:00:22 +07:00
Gaze
c731881a29 feat: set symbol limit to utf8.MaxRune 2024-04-15 23:34:33 +07:00
Gaze
ddd46da350 refactor(cli): add local flag
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 23:33:44 +07:00
Gaze
c646a0b3d9 feat: more tests 2024-04-15 23:32:09 +07:00
Gaze
fc9789f351 fix: edict flaws were ignored 2024-04-15 23:14:48 +07:00
Gaze
3c8c1d55f6 refactor: test structure in runestone_test.go 2024-04-15 23:14:28 +07:00
Gaze
19c1720dd5 doc: remove unused desc
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:31:33 +07:00
Gaze
aabd35c545 refactor: move main logic to command
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:28:44 +07:00
Gaze
7a61f383a8 feat(cli): create cli commands
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:22:42 +07:00
Gaze
de12e29c73 refactor: move main to root
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:22:20 +07:00
Gaze
594ac11b46 feat: mod tidy
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:21:44 +07:00
Gaze
7f8628e168 feat: remove annoying log
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:21:32 +07:00
Gaze
7ed2513178 refactor(config): separate init and get config func
Co-authored-by: Gaze <dev@gaze.network>
2024-04-15 22:21:19 +07:00
Gaze
46efbf3cff feat: include new rune entries in event hash and flushing 2024-04-15 22:05:30 +07:00
Gaze
72a5578c29 feat: add rune number to rune entry 2024-04-15 21:57:05 +07:00
Gaze
184c3d8ab5 feat: prevent processing of txs before activation height 2024-04-15 21:35:43 +07:00
Gaze
bb44cb6a04 fix: debug log unsafe 2024-04-15 20:56:50 +07:00
Gaze
1059e8279c fix: mint cap reached off by one 2024-04-15 20:56:42 +07:00
Gaze
270866f0a9 fix: create rune entry states only during flushes 2024-04-15 20:40:14 +07:00
Gaze
3ad58bf903 fix: actually use transactions to write db 2024-04-15 20:37:46 +07:00
Gaze
4c5a732913 feat: add tx hash and out index in tx output 2024-04-15 20:16:44 +07:00
Gaze
9350e4eb39 fix: sql ambiguous column 2024-04-15 20:14:35 +07:00
Gaze
921f7e8c7c Merge branch 'feature/bitcoin-indexer' into feature/cli-cmd 2024-04-15 20:03:28 +07:00
Gaze
a9183521f7 fix: check balance after populating current balance 2024-04-15 16:46:03 +07:00
Gaze
021c138248 feat: stop main if indexer failed 2024-04-15 16:41:08 +07:00
Gaze
c995e2e183 feat: add etching tx hash to rune entries 2024-04-15 16:38:44 +07:00
Gaze
151d7c2019 fix: golangci-lint 2024-04-15 16:31:03 +07:00
Gaze
d94925853e fix: not found error 2024-04-15 16:30:44 +07:00
Gaze
cc6a28e738 fix: repo 2024-04-15 16:26:39 +07:00
Gaze
c0a1092a17 chore: remove redundant log 2024-04-15 16:26:33 +07:00
Gaze
7174bb6221 fix: switch pk to block height 2024-04-15 16:19:08 +07:00
Gaze
5129e0c5f0 feat: more logs 2024-04-15 16:18:52 +07:00
Gaze
bb8e510673 fix: extract tapscript bug 2024-04-15 16:18:48 +07:00
Gaze
354673e9f0 feat: more logs 2024-04-15 15:54:28 +07:00
Gaze
9d5941ba4c chore: change testnet starting block 2024-04-15 15:54:08 +07:00
Gaze
a33234f452 fix: fix panic if empty pkscript 2024-04-15 15:53:49 +07:00
Gaze
303e0322a7 fix: make etching_terms nullable 2024-04-15 15:53:37 +07:00
Gaze
b53efd55ab Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-15 15:45:01 +07:00
Gaze
fb67435a39 fix: don't remove first block 2024-04-15 15:44:54 +07:00
Gaze
591bbe1614 fix: get starting indexed block 2024-04-15 15:01:50 +07:00
Gaze
0ba272b8f1 fix: unhandled errors in repo 2024-04-15 14:37:26 +07:00
Gaze
be7f2a4e39 chore: add more logs in processor 2024-04-15 14:30:58 +07:00
Gaze
6cd020d68a fix: unhandled error in repo 2024-04-15 14:30:49 +07:00
Gaze
2d2f5d8769 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-15 14:20:32 +07:00
Gaze
e3f640cca3 fix: call run() in newSubscription 2024-04-15 14:20:26 +07:00
Gaze
8b9bf66093 fix: remove log postgres credentials 2024-04-15 00:47:34 +07:00
Gaze
d6659ac1e3 fix: config example name 2024-04-15 00:46:55 +07:00
Gaze
bb0f9bc9f2 Merge branch 'feature/bitcoin-indexer' into feat/runes-module
# Conflicts:
#	config.example.yaml
2024-04-15 00:45:21 +07:00
Gaze
7dc2e9887c fix: config example 2024-04-15 00:44:52 +07:00
Gaze
e6b0b6e86c feat: implement bitcoin.Contract in bitcoin node datasource 2024-04-15 00:40:49 +07:00
Gaze
95b57c55f4 fix: config 2024-04-15 00:40:44 +07:00
Gaze
fa41d11273 feat: initialize rune indexer 2024-04-15 00:40:29 +07:00
Gaze
8a7b3b1675 feat: implement bitcoin.Contract in bitcoin node datasource 2024-04-15 00:40:16 +07:00
Gaze
be3f657bb6 fix: config 2024-04-15 00:40:00 +07:00
Gaze
1efa77aaef feat: move runes repository out of internal 2024-04-15 00:22:25 +07:00
Gaze
43bcc1e2fa Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module
# Conflicts:
#	go.mod
#	go.sum
2024-04-15 00:12:06 +07:00
Gaze
a6c7d779e1 feat: add starting block header 2024-04-15 00:10:24 +07:00
Gaze
88c8743dd1 feat: implement event hash 2024-04-14 22:23:41 +07:00
Gaze
fdb23ccbad feat: add tx index to entity 2024-04-14 21:41:44 +07:00
Gaze
8c808f027b feat: add cobra
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 21:00:51 +07:00
Gaze
5feb14c3ff feat(config): remove unused
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 20:58:15 +07:00
Gaze
16396e1c15 feat(config): use viper for configuration management
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 20:57:35 +07:00
Gaze
0ab35bd5ea feat: grouping module configs
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 19:58:52 +07:00
Gaze
9a1d4438d1 fix(btc): invalid import
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:58:23 +07:00
Gaze
1adc72c50d feat: update main
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:54:28 +07:00
Gaze
4f18189474 refactor(btc): move repo to public place
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:53:00 +07:00
Gaze
a330bd9e5e feat(btc): add logger detailed
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:26:34 +07:00
Gaze
156d437f71 feat: add Name() to interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:25:39 +07:00
Gaze
771de14209 feat(logger): update logger error interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:22:26 +07:00
Gaze
d924324246 feat(btc): add new instance for btc
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 18:18:46 +07:00
Gaze
4cfd111437 feat: move OutpointBalance json marshal logic to entity 2024-04-14 17:11:14 +07:00
Gaze
08a778f534 feat: uuse logger context
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 16:52:40 +07:00
Gaze
46122e3b23 feat: update log in config
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 16:51:00 +07:00
Gaze
1ef173b3ab feat: add app config
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 16:50:26 +07:00
Gaze
3722543da8 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-14 16:12:49 +07:00
Gaze
13c25423c6 fix: rename value to amount 2024-04-14 16:12:16 +07:00
Gaze
b342921ed9 chore: bump deps to fix bug
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 13:08:36 +07:00
Gaze
1f07bd4a6d fix(btc): cyclic closing stream and subscription 2024-04-14 13:06:48 +07:00
Gaze
d0f0e02cef chore: bump deps to fix bug 2024-04-14 12:44:46 +07:00
Gaze
4f48ebaafc refactor(btc): easy read close logic
Co-authored-by: Gaze <dev@gaze.network>
2024-04-14 03:44:38 +07:00
Gaze
45e87601a6 feat(btc): add Fetch Async impl 2024-04-14 03:37:43 +07:00
Gaze
f64b4f00b3 feat: flush outpoint balances 2024-04-14 01:42:57 +07:00
Gaze
36ed4fd9c9 feat: flush rune txs 2024-04-14 01:21:02 +07:00
Gaze
0033b51447 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-14 00:24:21 +07:00
Gaze
0607abba1f feat(btc): return error if context err
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:39:45 +07:00
Gaze
7c0f47002e feat(btc): update code
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:33:08 +07:00
Gaze
bd8b2cf114 feat: flush new balances 2024-04-13 22:28:46 +07:00
Gaze
58a3d1bbcc feat(btc): update fetch sync flow
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:28:13 +07:00
Gaze
2b374e83eb doc(btc): update note
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:25:47 +07:00
Gaze
0c13ae9675 feat(btc): update error loggger
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:25:20 +07:00
Gaze
ba273a9237 feat(btc): update reorg handling flow
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:23:42 +07:00
Gaze
b8cc03341d feat(btc): unused fields
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:03:38 +07:00
Gaze
303a6d7cec feat(btc): update indexer flow
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:03:18 +07:00
Gaze
ec4b3b188f feat(btc): update btc subscription and indexer flow
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 22:00:44 +07:00
Gaze
64bc8f3214 feat(btc): handle new fetch subscription
Co-authored-by: Gaze <dev@gaze.network>
2024-04-13 21:03:39 +07:00
Gaze
39244f1864 fix: more init 2024-04-13 20:57:51 +07:00
Gaze
2241f38d97 feat: set completed at 2024-04-13 20:44:51 +07:00
Gaze
0480a4dcbf chore: remove TODO 2024-04-13 20:02:21 +07:00
Gaze
6b95c376bb feat: add version check and seed genesis rune 2024-04-13 17:47:29 +07:00
Gaze
9a44565545 feat: add turbo flag 2024-04-13 16:29:15 +07:00
Gaze
e550136ef1 fix: warnings 2024-04-13 16:20:46 +07:00
Gaze
ec62b113d1 fix: mapping error 2024-04-13 16:19:37 +07:00
Gaze
939c2aaf9c fix: off by one 2024-04-13 15:44:41 +07:00
Gaze
4ab2dcf098 feat: implement RevertData() 2024-04-13 15:43:50 +07:00
Gaze
7929234239 feat: remove processor state and add prev_hash to indexed block 2024-04-13 15:29:05 +07:00
Gaze
901946af38 feat: flush new entry states and indexed blocks 2024-04-13 01:59:54 +07:00
Gaze
0f1ec2a21e feat: implement more tables 2024-04-13 01:27:21 +07:00
Gaze
f72c9da8c0 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-12 20:45:14 +07:00
Gaze
5a86b5560c feat(btc): use subscriber in Fetch function
Co-authored-by: Gaze <dev@gaze.network>
2024-04-12 01:27:14 +07:00
Gaze
ce3d0a2414 feat(indexer): update interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-12 01:26:27 +07:00
Gaze
0e85fca98c feat(datasource): implement client subscriber
Co-authored-by: Gaze <dev@gaze.network>
2024-04-12 01:25:57 +07:00
Gaze
7616f9c4b0 feat: implement get indexed block 2024-04-12 00:39:08 +07:00
Gaze
a712c18bb3 feat: update uint128FromNumeric 2024-04-12 00:21:00 +07:00
Gaze
e369d59522 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-12 00:04:26 +07:00
Gaze
f444b679be feat: implement current block 2024-04-12 00:02:03 +07:00
Gaze
ea4bccffd7 doc(btc): uupdate func description
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 23:17:26 +07:00
Gaze
2c54dc9245 feat(btc): update interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 23:06:45 +07:00
Gaze
98099f3a66 doc(btc): add btc datasource desc
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 23:04:30 +07:00
Gaze
2668108f2b fix: new interface 2024-04-11 21:39:48 +07:00
Gaze
e1a41c0853 chore: add TODO 2024-04-11 21:39:33 +07:00
Gaze
12a18d7bfd feat(btc): prepare bitcoin node datasource
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 20:44:21 +07:00
Gaze
96205fc880 fix: remove errs.ErrorKind usage 2024-04-11 20:38:24 +07:00
Gaze
f588587d69 Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-11 20:35:47 +07:00
Gaze
d0e1e2bd50 fix: change types in db 2024-04-11 20:34:26 +07:00
Gaze
78202c7b92 fix(indexer): invalid ch direction
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 20:26:06 +07:00
Gaze
2003b1c81d feat(indexer): ensure impl
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 20:21:27 +07:00
Gaze
8092287609 feat(indexer): update interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 20:17:49 +07:00
Gaze
7be2fdba83 feat(indexer): update indexer interface
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 20:12:16 +07:00
Gaze
2ac8080ebf feat: implement api handlers 2024-04-11 20:12:13 +07:00
Gaze
2363b19556 feat(errs): business logic error mus inherit from
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 17:32:30 +07:00
Gaze
4c0e16ccb0 feat(errs): refector common errors
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 17:21:39 +07:00
Gaze
3d0a582460 feat(stacktrace): remove empty pcs
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 17:21:07 +07:00
Gaze
d89d79ac0f fix(stacktrace): extract all error stacktrace
Co-authored-by: Gaze <dev@gaze.network>
2024-04-11 17:00:01 +07:00
Gaze
458dcc118e Merge branch 'feature/bitcoin-indexer' of github.com:gaze-network/indexer-network into feature/bitcoin-indexer 2024-04-11 01:35:27 +07:00
Gaze
40c5a04f08 feat: remove unused
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-11 01:35:22 +07:00
Gaze
f83f71fa84 refactor: move Process() logic to separate file 2024-04-11 00:51:47 +07:00
Gaze
29fb87abcb feat: finish runes Process() logic 2024-04-11 00:46:25 +07:00
Gaze
31a81de513 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-10 22:41:35 +07:00
Gaze
d2afb9d11b feat: add tx index in types 2024-04-10 22:41:28 +07:00
Gaze
86b38cbf79 Merge branch 'feature/bitcoin-indexer' into feat/runes-module 2024-04-10 22:09:03 +07:00
Gaze
e07b0ed635 feat: add btcclient contract 2024-04-10 22:08:36 +07:00
Gaze
1b5181c322 chore(btc): type-safety forr repo
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 20:05:45 +07:00
Gaze
89ffdf03f5 feat(btc): update insert block repo
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 20:03:54 +07:00
Gaze
e11aeee117 feat(btc): update mapper func
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 20:03:40 +07:00
Gaze
3be5c0c9f6 feat(errs): add internal server error
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 20:03:07 +07:00
Gaze
e0522c94de feat(btc): handling critical case for insert duplicate utxo spent
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 19:36:46 +07:00
Gaze
0ad604ddf7 feat: use SELECT FOR UPDATE for rune entries 2024-04-10 16:51:58 +07:00
Gaze
cfab86e94c feat: start transaction in Process() 2024-04-10 16:44:11 +07:00
Gaze
d584e290eb feat: implement runes data write funcs 2024-04-10 16:36:43 +07:00
Gaze
535e5b7223 feat: implement runes data reader funcs 2024-04-10 15:48:20 +07:00
Gaze
4d580c09ed refactor: runes dg 2024-04-10 14:20:02 +07:00
Gaze
ddd759ea19 refactor: rename variables in processor 2024-04-10 14:19:02 +07:00
Gaze
f5b3efe551 refactor: split reader and writer datagateway 2024-04-10 14:18:54 +07:00
Gaze
c9df3ecd45 fix: merge errors 2024-04-10 14:08:01 +07:00
Gaze
142692fd6e Merge remote-tracking branch 'origin/feature/bitcoin-indexer' into feat/runes-module 2024-04-10 14:05:15 +07:00
Gaze
1a1d1ac2c3 Merge branch 'develop' into feature/bitcoin-indexer 2024-04-10 10:25:16 +07:00
Gaze
6583de1318 feat(btc): db model mapper funcitons
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 10:24:56 +07:00
Gaze
d2baa903e1 docs(btc): add fields desc
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 10:18:30 +07:00
Gaze
20a5caf4c1 feat(btc): add height to tx and block
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 10:02:46 +07:00
Gaze
9d87f6b83c feat(logger): add named error
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:45:02 +07:00
Gaze
bec2f11f45 Merge branch 'develop' into feature/bitcoin-indexer 2024-04-10 09:28:39 +07:00
Gaze
d1dca25b2a feat(logger): fix long error data
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:28:13 +07:00
Gaze
badfba7ff7 feat(logger): use const key
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:21:02 +07:00
Gaze
3f03e54256 feat(logger): use stacktrace frame to log
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:20:01 +07:00
Gaze
9b1e567354 refactor(stacktrace): change folder dir
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:13:33 +07:00
Gaze
c356d43e76 feat(stacktrace): add ParsePCS func
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:08:08 +07:00
Gaze
9859ed3626 feat(logger): update stacktrace api
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:04:18 +07:00
Gaze
b923d83432 feat(stacktrace): use runtime.Frame instead manual parse
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 09:04:00 +07:00
Gaze
76ec00420d feat(stacktrace): upgrade max dept of stacks
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 07:56:31 +07:00
Gaze
191b973eae feat(stacktrace): use ptr instead copy value
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 07:52:38 +07:00
Gaze
d5338a1e6f feat(logger): supprt stacktrace attr
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 07:42:52 +07:00
Gaze
34bca5f6c4 feat(logger): support Reflect attr type
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 07:41:52 +07:00
Gaze
be6ebe9a4d feat(logger): add basic attr types
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 07:41:07 +07:00
Gaze
eff202c7fe feat(logger): create slogx for slog extension
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 04:40:12 +07:00
Gaze
8fdc705bae feat(btc): use slogx
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 04:40:06 +07:00
Gaze
60d4c8ba51 feat(logger): create slogx for slog extension
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 04:39:52 +07:00
Gaze
679c012a90 Merge branch 'develop' into feature/bitcoin-indexer 2024-04-10 04:10:06 +07:00
Gaze
2dc3ae7561 ci(lint): add slog linter config
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 04:09:56 +07:00
Gaze
071a09745b feat(btc): use logger context
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 03:53:22 +07:00
Gaze
3ab7a5fbf5 Merge branch 'develop' into feature/bitcoin-indexer 2024-04-10 03:48:45 +07:00
Gaze
78ef151903 feat: add log multi middleware handler
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 03:43:19 +07:00
Gaze
dc60975437 feat(logger): add logger context utilities
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 03:41:40 +07:00
Gaze
671c8efee7 fix(logger): invalid source call stack
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 03:30:09 +07:00
Gaze
721453a62d fix: didn't import logger
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 03:13:18 +07:00
Gaze
8f59a6b93f feat: add logger context
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:53:29 +07:00
Gaze
cc91dcbb5f Merge branch 'develop' into feature/bitcoin-indexer 2024-04-10 02:49:56 +07:00
Gaze
a755078394 feat: update logger for cli application
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:49:48 +07:00
Gaze
cfa47ab508 feat: update logger for cli application
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:49:24 +07:00
Gaze
a4d609d295 wip(btc): add dataabase implementation
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:22:11 +07:00
Gaze
25ce73bf29 feat(btc): add btc indexer implementation
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:21:52 +07:00
Gaze
88f4335446 feat(btc): update interface
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 02:21:39 +07:00
Gaze
f510f00d04 feat(btc): update interface
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 01:44:14 +07:00
Gaze
40f2d7b6d3 feat(btc): update interface
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-10 01:42:56 +07:00
Gaze
492d1024aa fix: unused imports 2024-04-10 01:08:21 +07:00
Gaze
5df8ce2c0e fix: use wire.OutPoint 2024-04-10 01:07:42 +07:00
Gaze
e218d90f79 WIP: Process() rune logic 2024-04-10 00:49:47 +07:00
Gaze
b3f44120b9 feat: update postgres trace logging
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 23:34:48 +07:00
Gaze
e035cf8a2a fix: rune types 2024-04-09 23:29:32 +07:00
Gaze
1ac5ee1e14 feat: use logger library
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 23:28:12 +07:00
Gaze
d8705b9890 fix: types 2024-04-09 23:27:07 +07:00
Gaze
676a0d41f4 feat: more runes queries 2024-04-09 23:20:41 +07:00
Gaze
4958b1d81b fix: types 2024-04-09 23:20:41 +07:00
Gaze
eebc3c1f75 refactor: leb128 to pkg 2024-04-09 23:20:41 +07:00
Gaze
51482550f9 refactor: runes processor 2024-04-09 23:20:41 +07:00
Gaze
cc88231813 feat: add database queries and migrations 2024-04-09 23:20:41 +07:00
Gaze
0767866470 feat: implement rune entry 2024-04-09 23:20:41 +07:00
Gaze
589a0ebebe feat: init runes processor 2024-04-09 23:20:41 +07:00
Gaze
6709829d3a refactor: move runes logic to internal 2024-04-09 23:20:24 +07:00
Gaze
6d300f40fe feat: add more runestone tests 2024-04-09 23:20:24 +07:00
Gaze
d7b3972169 feat: add basic tests for runestone 2024-04-09 23:20:24 +07:00
Gaze
c010700e65 feat: add TestMaxSpacers 2024-04-09 23:20:24 +07:00
Gaze
a07f4cd8b9 feat: implement runes structs and methods
This reverts commit 1f13234118.
2024-04-09 23:20:24 +07:00
Gaze
62a9670e34 Merge branch 'develop' into feature/bitcoin-indexer 2024-04-09 23:16:19 +07:00
Gaze
d04a973150 feat: add logger library
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 23:15:38 +07:00
Gaze
ff828eeb76 chore: add postgres client library
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 20:03:07 +07:00
Gaze
94ca9a9199 wip(btc:): prepare processor
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 19:31:45 +07:00
Gaze
c0c21c2a1b feat(btc): create internal bitcoin types
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 19:28:02 +07:00
Gaze
b127b27282 feat: remove unused
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-09 18:58:57 +07:00
Gaze
1f13234118 chore: remove runes module to prepare for review 2024-04-09 01:33:46 +07:00
Gaze
cab6582ae3 Merge branch 'feat/rune-entities' into develop 2024-04-09 01:28:01 +07:00
Gaze
afc79964ba feat: implement Runestone.Encipher 2024-04-09 00:56:14 +07:00
Gaze
7c9bbf3d75 fix: use pointer of primitives in etchings 2024-04-09 00:24:47 +07:00
Gaze
9f4cba0c35 fix: typo 2024-04-08 23:07:02 +07:00
Gaze
69e72245d6 chore(btc): update drop table index
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 22:47:48 +07:00
Gaze
0bd3347af6 feat(btc): generate code for sqlc orm
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 22:44:49 +07:00
Gaze
25a4c9d2af feat(btc): add orm and basic queries
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 22:44:10 +07:00
Gaze
3d20d29b5d chore(btc): prevent use same utxo for txin
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 22:35:43 +07:00
Gaze
fbc58e5b6e feat: implement SpacedRune 2024-04-08 22:14:13 +07:00
Gaze
fb2dbfec85 chore(btc): create bitcoin schema
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 20:28:45 +07:00
Gaze
1e3a9310c3 chore: go mod tidy
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 12:51:02 +07:00
Gaze
f14ef4b2db wip(btc): add refactor plan
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 12:39:05 +07:00
Gaze
357f97ecee refactor(btc): use i instead b
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 12:37:01 +07:00
Gaze
da422be418 feat(btc): update processor interface
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-08 12:34:29 +07:00
Gaze
9aed3511e6 feat(btc): update block type 2024-04-08 08:02:46 +07:00
Gaze
92e55da4c3 feat: rewrite with uint128.Uint128 2024-04-07 17:52:48 +07:00
Gaze
21af4491c3 feat: add genesis rune test 2024-04-07 15:09:22 +07:00
Gaze
bc8c8bd596 feaT: implement runestone, flag, tag, and fix rune_id 2024-04-03 17:54:14 +07:00
Gaze
98ac9093aa feat: add more errors 2024-04-03 17:54:06 +07:00
Gaze
082aaa4197 feat: implement leb128 2024-04-03 17:54:01 +07:00
Gaze
2684b064fc feat(btc): extract prepare range func
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-03 05:10:02 +07:00
Gaze
86f7001875 feat: create indexer client cmd
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-03 05:05:47 +07:00
Gaze
15711c8655 feat(btc): prepare range of blocks to sync
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-03 04:44:12 +07:00
Gaze
89317ac4e9 doc: update indexer desc
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-03 03:20:58 +07:00
Gaze
1d12721924 feat: update btc indexer
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-03 03:18:33 +07:00
Gaze
38762d62f1 fix: rename package 2024-04-02 23:36:15 +07:00
Gaze
e77d401f56 ci: prevent team to import pkg/errors
Co-authored-by: Gaze <gazenw@users.noreply.github.com>
2024-04-02 23:24:34 +07:00
Gaze
7d49b7bdba Merge remote-tracking branch 'origin/feat/rune-entities' into develop 2024-04-02 23:12:20 +07:00
Gaze
779c868fa9 feat: update docs 2024-04-02 23:07:08 +07:00
Gaze
762c40af0a refactor: switch to errs.ErrorKind 2024-04-02 22:56:46 +07:00
Gaze
5b74fde9f4 refactor: minor refactor 2024-04-02 22:35:29 +07:00
Gaze
4fd6963cc8 feat: implement etching 2024-04-02 22:35:29 +07:00
Gaze
1b6356a691 feat: implement rune, rune_id, and edict 2024-04-02 22:35:29 +07:00
Gaze
f01cd223d8 feat: init bitcoin protocol indexer 2024-04-02 20:51:52 +07:00
Gaze
c9a8d36f89 wip: create btc rellyer indexer 2024-04-01 23:19:22 +07:00
106 changed files with 3932 additions and 4465 deletions

View File

@@ -1,18 +0,0 @@
.git
.gitignore
.github
.vscode
**/*.md
**/*.log
.DS_Store
# Docker
Dockerfile
.dockerignore
docker-compose.yml
# Go
.golangci.yaml
cmd.local
config.*.y*ml
config.y*ml

View File

@@ -1 +0,0 @@
# Contributor Covenant Code of Conduct

View File

@@ -1,34 +0,0 @@
# Contributing
Please note: we have a [code of conduct](https://github.com/gaze-network/gaze-indexer/blob/main/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the Gaze Network project.
## Pull Requests or Commits
#### Message structured
```plaintext
<type>(optional scope):<description>
```
The `<type>` must be one of the following:
> feat:, refactor:, fix:, doc:, style:, perf:, test:, chore:, ci:, build:
- feat(runes): add Runes module to the project
- refactor: change project structure
- fix(btc): fix chain reorganization issue
- doc: update \`run\` command documentation
- style: fix linting issues
- perf: improve performance of the bitcoin node datasource
- test(runes): add unit tests for etching logic
- chore: bump dependencies versions
- ci: update CI configuration
- build: update Dockerfile to use alpine
# 👍 Contribute
If you want to say **thank you** and/or support the active development of `Fiber`:
1. Add a [GitHub Star](https://github.com/gaze-network/gaze-indexer/stargazers) to the project.
2. Follow and mention our [Twitter (𝕏)](https://twitter.com/Gaze_Network).
3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog.

View File

@@ -1,22 +0,0 @@
## Description
Please provide a clear and concise description of the changes you've made and the problem they address. Include the purpose of the change, any relevant issues it solves, and the benefits it brings to the project. If this change introduces new features or adjustments, highlight them here.
Fixes # (issue)
## Type of change
What types of changes does your code introduce to Appium?
_Put an `x` in the boxes that apply_
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Enhancement (improvement to existing features and functionality)
- [ ] Documentation update (changes to documentation)
- [ ] Performance improvement (non-breaking change which improves efficiency)
- [ ] Code consistency (non-breaking change which improves code reliability and robustness)
## Commit formatting
Please follow the commit message conventions for an easy way to identify the purpose or intention of a commit. Check out our commit message conventions in the [CONTRIBUTING.md](https://github.com/gaze-network/gaze-indexer/blob/main/.github/CONTRIBUTING.md#pull-requests-or-commits)

View File

@@ -1,77 +0,0 @@
name: Code Analysis & Test
on:
workflow_dispatch:
pull_request:
branches:
- develop
- main
paths:
- "go.mod"
- "go.sum"
- "**.go"
- ".golangci.yaml"
- ".github/workflows/code-analysis.yml"
jobs:
lint:
strategy:
matrix:
os: ["ubuntu-latest"]
name: Lint (${{ matrix.os }})
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: "0"
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true # caching and restoring go modules and build outputs.
- name: Lint
uses: reviewdog/action-golangci-lint@v2
with: # https://github.com/reviewdog/action-golangci-lint#inputs
go_version_file: "go.mod"
workdir: ./
golangci_lint_flags: "--config=./.golangci.yaml --verbose --new-from-rev=${{ github.event.pull_request.base.sha }}"
fail_on_error: true
test:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
go-version: ["1.22.x", "1.x"] # minimum version and latest version
name: Test (${{ matrix.os }}/${{ matrix.go-version }})
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: "0"
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
cache: true # caching and restoring go modules and build outputs.
- run: echo "GOVERSION=$(go version)" >> $GITHUB_ENV
- name: Build
run: go build -v ./...
- name: Test
run: go test -json ./... > test_output.json
- name: Summary Test Results
if: always()
uses: robherley/go-test-action@v0
with:
fromJSONFile: test_output.json
- name: Annotate Test Results
if: always()
uses: guyarb/golang-test-annotations@v0.5.1
with:
test-results: test_output.json

View File

@@ -1,28 +0,0 @@
name: Sqlc ORM Framework Verify
on:
workflow_dispatch:
pull_request:
branches:
- develop
- main
paths:
- "sqlc.yaml"
- "**.sql"
- ".github/workflows/sqlc-verify.yml"
jobs:
sqlc-diff:
name: Sqlc Diff Checker
runs-on: "ubuntu-latest" # "self-hosted", "ubuntu-latest", "macos-latest", "windows-latest"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: "0"
- name: Setup Sqlc
uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: "1.26.0"
- name: Check Diff
run: sqlc diff

2
.gitignore vendored
View File

@@ -3,6 +3,8 @@
# Eg. ignore.foo_test.go, ignore.credentials.json, ignore.config.yml
ignore.*
/key
**/cmd.local/**
**/cmd.local.**/**

View File

@@ -3,15 +3,15 @@ FROM golang:1.22 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod/ go mod download
RUN go mod download
COPY ./ ./
ENV GOOS=linux
ENV CGO_ENABLED=0
RUN --mount=type=cache,target=/go/pkg/mod/ \
go build -o main ./main.go
RUN go build \
-o main ./main.go
FROM alpine:latest
@@ -19,10 +19,9 @@ WORKDIR /app
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/main .
COPY --from=builder /app/modules ./modules
# You can set TZ identifier to change the timezone, See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# ENV TZ=US/Central
# You can set `TZ` environment variable to change the timezone
ENTRYPOINT ["/app/main"]
CMD ["/app/main", "run"]

674
LICENSE
View File

@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

156
README.md
View File

@@ -1,155 +1 @@
<!-- omit from toc -->
# 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 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.
Gaze Indexer serves as a foundation for building ANY meta-protocol indexers, with efficient data fetching, reorg detection, and database migration tool.
This allows developers to focus on what **truly** matters: Meta-protocol indexing logic. New meta-protocols can be easily added by implementing new modules.
- [Modules](#modules)
- [1. Runes](#1-runes)
- [Installation](#installation)
- [Prerequisites](#prerequisites)
- [1. Hardware Requirements](#1-hardware-requirements)
- [2. Prepare Bitcoin Core RPC server.](#2-prepare-bitcoin-core-rpc-server)
- [3. Prepare database.](#3-prepare-database)
- [4. Prepare `config.yaml` file.](#4-prepare-configyaml-file)
- [Install with Docker (recommended)](#install-with-docker-recommended)
- [Install from source](#install-from-source)
## Modules
### 1. Runes
The Runes Indexer is our first meta-protocol indexer. It indexes Runes states, transactions, runestones, and balances using Bitcoin transactions.
It comes with a set of APIs for querying historical Runes data. See our [API Reference](https://api-docs.gaze.network) for full details.
## Installation
### Prerequisites
#### 1. Hardware Requirements
Each module requires different hardware requirements.
| Module | CPU | RAM |
| ------ | --------- | ---- |
| Runes | 0.5 cores | 1 GB |
#### 2. Prepare Bitcoin Core RPC server.
Gaze Indexer needs to fetch transaction data from a Bitcoin Core RPC, either self-hosted or using managed providers like QuickNode.
To self host a Bitcoin Core, see https://bitcoin.org/en/full-node.
#### 3. Prepare database.
Gaze Indexer has first-class support for PostgreSQL. If you wish to use other databases, you can implement your own database repository that satisfies each module's Data Gateway interface.
Here is our minimum database disk space requirement for each module.
| Module | Database Storage (current) | Database Storage (in 1 year) |
| ------ | -------------------------- | ---------------------------- |
| Runes | 10 GB | 150 GB |
#### 4. Prepare `config.yaml` file.
```yaml
# config.yaml
logger:
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
debug: false
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
network: mainnet
# Bitcoin Core RPC configuration options.
bitcoin_node:
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
user: "" # Username to authenticate with Bitcoin Core RPC
pass: "" # Password to authenticate with Bitcoin Core RPC
disable_tls: false # Set to true to disable tls
# Block reporting configuration options. See Block Reporting section for more details.
reporting:
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
# HTTP server configuration options.
http_server:
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
# Meta-protocol modules configuration options.
modules:
# Configuration options for Runes module. Can be removed if not used.
runes:
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
datasource: "bitcoin-node" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
api_handlers: # API handlers to enable. current supported handlers: "http"
- http
postgres:
host: "localhost"
port: 5432
user: "postgres"
password: "password"
db_name: "postgres"
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
```
### Install with Docker (recommended)
We will be using `docker-compose` for our installation guide. Make sure the `docker-compose.yaml` file is in the same directory as the `config.yaml` file.
```yaml
# docker-compose.yaml
services:
gaze-indexer:
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
container_name: gaze-indexer
restart: unless-stopped
ports:
- 8080:8080 # Expose HTTP server port to host
volumes:
- "./config.yaml:/app/config.yaml" # mount config.yaml file to the container as "/app/config.yaml"
command: ["/app/main", "run", "--modules", "runes"] # Put module flags after "run" commands to select which modules to run.
```
### Install from source
1. Install `go` version 1.22 or higher. See Go installation guide [here](https://go.dev/doc/install).
2. Clone this repository.
```bash
git clone https://github.com/gaze-network/gaze-indexer.git
cd gaze-indexer
```
3. Build the main binary.
```bash
# Get dependencies
go mod download
# Build the main binary
go build -o gaze main.go
```
4. Run database migrations with the `migrate` command and module flags.
```bash
./gaze migrate up --runes --database postgres://postgres:password@localhost:5432/postgres
```
5. Start the indexer with the `run` command and module flags.
```bash
./gaze run --modules runes
```
If `config.yaml` is not located at `./app/config.yaml`, use the `--config` flag to specify the path to the `config.yaml` file.
```bash
./gaze run --modules runes --config /path/to/config.yaml
```
# Gaze Indexer Network

View File

@@ -14,14 +14,14 @@ var (
// root command
cmd = &cobra.Command{
Use: "gaze",
Long: `Description of gaze indexer`,
Long: `Gaze in a Bitcoin meta-protocol indexer`,
}
// sub-commands
// sub-commandsf
cmds = []*cobra.Command{
NewVersionCommand(),
NewRunCommand(),
NewMigrateCommand(),
NewGenerateKeypairCommand(),
}
)
@@ -44,7 +44,7 @@ func Execute(ctx context.Context) {
// Initialize logger
if err := logger.Init(config.Logger); err != nil {
logger.PanicContext(ctx, "Something went wrong, can't init logger", slogx.Error(err), slog.Any("config", config.Logger))
logger.Panic("Failed to initialize logger: %v", slogx.Error(err), slog.Any("config", config.Logger))
}
})
@@ -53,7 +53,7 @@ func Execute(ctx context.Context) {
// Execute command
if err := cmd.ExecuteContext(ctx); err != nil {
// Cobra will print the error message by default
logger.DebugContext(ctx, "Error executing command", slogx.Error(err))
// use cobra to log error message by default
logger.Debug("Failed to execute root command", slogx.Error(err))
}
}

100
cmd/cmd_generate_keypair.go Normal file
View File

@@ -0,0 +1,100 @@
package cmd
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/pkg/crypto"
"github.com/spf13/cobra"
)
type generateKeypairCmdOptions struct {
Path string
}
func NewGenerateKeypairCommand() *cobra.Command {
opts := &generateKeypairCmdOptions{}
cmd := &cobra.Command{
Use: "generate-keypair",
Short: "Generate new public/private keypair for encryption and signature generation",
RunE: func(cmd *cobra.Command, args []string) error {
return generateKeypairHandler(opts, cmd, args)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.Path, "path", "/data/keys", `Path to save to key pair file`)
return cmd
}
func generateKeypairHandler(opts *generateKeypairCmdOptions, _ *cobra.Command, _ []string) error {
fmt.Printf("Generating key pair\n")
privKeyBytes := make([]byte, 32)
_, err := rand.Read(privKeyBytes)
if err != nil {
return errors.Wrap(errs.SomethingWentWrong, "random bytes")
}
_, pubKey := btcec.PrivKeyFromBytes(privKeyBytes)
serializedPubKey := pubKey.SerializeCompressed()
// fmt.Println(hex.EncodeToString(privKeyBytes))
fmt.Printf("Public key: %s\n", hex.EncodeToString(serializedPubKey))
err = os.MkdirAll(opts.Path, 0o755)
if err != nil {
return errors.Wrap(errs.SomethingWentWrong, "create directory")
}
privateKeyPath := path.Join(opts.Path, "priv.key")
_, err = os.Stat(privateKeyPath)
if err == nil {
fmt.Printf("Existing private key found at %s\n[WARNING] THE EXISTING PRIVATE KEY WILL BE LOST\nType [replace] to replace existing private key: ", privateKeyPath)
var ans string
fmt.Scanln(&ans)
if ans != "replace" {
fmt.Printf("Keypair generation aborted\n")
return nil
}
}
err = os.WriteFile(privateKeyPath, []byte(hex.EncodeToString(privKeyBytes)), 0o644)
if err != nil {
return errors.Wrap(err, "write private key file")
}
fmt.Printf("Private key saved at %s\n", privateKeyPath)
wifKeyPath := path.Join(opts.Path, "priv_wif_mainnet.key")
client, err := crypto.New(hex.EncodeToString(privKeyBytes))
if err != nil {
return errors.Wrap(err, "new crypto client")
}
wifKey, err := client.WIF(&chaincfg.MainNetParams)
if err != nil {
return errors.Wrap(err, "get WIF key")
}
err = os.WriteFile(wifKeyPath, []byte(wifKey), 0o644)
if err != nil {
return errors.Wrap(err, "write WIF private key file")
}
fmt.Printf("WIF private key saved at %s\n", wifKeyPath)
publicKeyPath := path.Join(opts.Path, "pub.key")
err = os.WriteFile(publicKeyPath, []byte(hex.EncodeToString(serializedPubKey)), 0o644)
if err != nil {
return errors.Wrap(errs.SomethingWentWrong, "write public key file")
}
fmt.Printf("Public key saved at %s\n", publicKeyPath)
return nil
}

View File

@@ -1,20 +0,0 @@
package cmd
import (
"github.com/gaze-network/indexer-network/cmd/migrate"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/spf13/cobra"
)
func NewMigrateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "migrate",
Short: "Migrate database schema",
}
cmd.AddCommand(
migrate.NewMigrateUpCommand(),
migrate.NewMigrateDownCommand(),
)
return cmd
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"runtime"
@@ -15,249 +14,299 @@ import (
"github.com/btcsuite/btcd/rpcclient"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/indexers"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/bitcoin"
"github.com/gaze-network/indexer-network/modules/bitcoin/btcclient"
btcdatagateway "github.com/gaze-network/indexer-network/modules/bitcoin/datagateway"
btcpostgres "github.com/gaze-network/indexer-network/modules/bitcoin/repository/postgres"
"github.com/gaze-network/indexer-network/modules/runes"
"github.com/gaze-network/indexer-network/pkg/automaxprocs"
runesapi "github.com/gaze-network/indexer-network/modules/runes/api"
runesdatagateway "github.com/gaze-network/indexer-network/modules/runes/datagateway"
runespostgres "github.com/gaze-network/indexer-network/modules/runes/repository/postgres"
runesusecase "github.com/gaze-network/indexer-network/modules/runes/usecase"
"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"
"github.com/gaze-network/indexer-network/pkg/reportingclientv2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/favicon"
fiberrecover "github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/samber/do/v2"
"github.com/samber/lo"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
// Register Modules
var Modules = do.Package(
do.LazyNamed("runes", runes.New),
)
type runCmdOptions struct {
APIOnly bool
Bitcoin bool
Runes bool
}
func NewRunCommand() *cobra.Command {
opts := &runCmdOptions{}
// Create command
runCmd := &cobra.Command{
Use: "run",
Short: "Start indexer-network service",
RunE: func(cmd *cobra.Command, args []string) error {
if err := automaxprocs.Init(); err != nil {
logger.Error("Failed to set GOMAXPROCS", slogx.Error(err))
}
return runHandler(cmd, args)
return runHandler(opts, cmd, args)
},
}
// TODO: separate flags and bind flags to each module cmd package.
// Add local flags
flags := runCmd.Flags()
flags.Bool("api-only", false, "Run only API server")
flags.String("modules", "", "Enable specific modules to run. E.g. `runes,brc20`")
flags.BoolVar(&opts.APIOnly, "api-only", false, "Run only API server")
flags.BoolVar(&opts.Bitcoin, "bitcoin", false, "Enable Bitcoin indexer module")
flags.String("bitcoin-db", "postgres", `Database to store bitcoin data. current supported databases: "postgres"`)
flags.BoolVar(&opts.Runes, "runes", false, "Enable Runes indexer module")
flags.String("runes-db", "postgres", `Database to store runes data. current supported databases: "postgres"`)
flags.String("runes-datasource", "bitcoin-node", `Datasource to fetch bitcoin data for processing Meta-Protocol data. current supported datasources: "bitcoin-node" | "database"`)
// Bind flags to configuration
config.BindPFlag("api_only", flags.Lookup("api-only"))
config.BindPFlag("enable_modules", flags.Lookup("modules"))
config.BindPFlag("modules.bitcoin.database", flags.Lookup("bitcoin-db"))
config.BindPFlag("modules.runes.database", flags.Lookup("runes-db"))
config.BindPFlag("modules.runes.datasource", flags.Lookup("runes-datasource"))
return runCmd
}
const (
shutdownTimeout = 60 * time.Second
)
type HttpHandler interface {
Mount(router fiber.Router) error
}
func runHandler(cmd *cobra.Command, _ []string) error {
func runHandler(opts *runCmdOptions, cmd *cobra.Command, _ []string) error {
conf := config.Load()
// Validate inputs and configurations
{
if !conf.Network.IsSupported() {
return errors.Wrapf(errs.Unsupported, "%q network is not supported", conf.Network.String())
}
}
// Initialize application process context
// Initialize context
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()
injector := do.New(Modules)
do.ProvideValue(injector, conf)
do.ProvideValue(injector, ctx)
// Add logger context
ctx = logger.WithContext(ctx, slogx.Stringer("network", conf.Network))
// Initialize Bitcoin RPC client
do.Provide(injector, func(i do.Injector) (*rpcclient.Client, error) {
conf := do.MustInvoke[config.Config](i)
// Load private key
privKeyPath := conf.NodeKey.Path
if privKeyPath == "" {
privKeyPath = "/data/keys/priv.key"
}
privKeyByte, err := os.ReadFile(privKeyPath)
if err != nil {
logger.PanicContext(ctx, "Failed to read private key file", slogx.Error(err))
}
client, err := rpcclient.New(&rpcclient.ConnConfig{
Host: conf.BitcoinNode.Host,
User: conf.BitcoinNode.User,
Pass: conf.BitcoinNode.Pass,
DisableTLS: conf.BitcoinNode.DisableTLS,
HTTPPostMode: true,
}, nil)
// Initialize Bitcoin Core RPC Client
client, err := rpcclient.New(&rpcclient.ConnConfig{
Host: conf.BitcoinNode.Host,
User: conf.BitcoinNode.User,
Pass: conf.BitcoinNode.Pass,
DisableTLS: conf.BitcoinNode.DisableTLS,
HTTPPostMode: true,
}, nil)
if err != nil {
logger.PanicContext(ctx, "Failed to create Bitcoin Core RPC Client", slogx.Error(err))
}
defer client.Shutdown()
logger.InfoContext(ctx, "Connecting to Bitcoin Core RPC Server...", slogx.String("host", conf.BitcoinNode.Host))
if err := client.Ping(); err != nil {
logger.PanicContext(ctx, "Failed to ping Bitcoin Core RPC Server", slogx.Error(err))
}
logger.InfoContext(ctx, "Connected to Bitcoin Core RPC Server")
// Validate network
if !conf.Network.IsSupported() {
return errors.Wrapf(errs.Unsupported, "%q network is not supported", conf.Network.String())
}
// TODO: create module command package.
// each module should have its own command package and main package will routing the command to the module command package.
// TODO: refactor module name to specific type instead of string?
httpHandlers := make(map[string]HttpHandler, 0)
// use gracefulEG to coordinate graceful shutdown after context is done. (e.g. shut down http server, shutdown logic of each module, etc.)
gracefulEG, gctx := errgroup.WithContext(context.Background())
var reportingClient *reportingclientv2.ReportingClient
if !conf.Reporting.Disabled {
reportingClient, err = reportingclientv2.New(conf.Reporting, string(privKeyByte)) // TODO: read private key from file
if err != nil {
return nil, errors.Wrap(err, "invalid Bitcoin node configuration")
logger.PanicContext(ctx, "Failed to create reporting client", slogx.Error(err))
}
}
// Check Bitcoin RPC connection
{
start := time.Now()
logger.InfoContext(ctx, "Connecting to Bitcoin Core RPC Server...", slogx.String("host", conf.BitcoinNode.Host))
if err := client.Ping(); err != nil {
return nil, errors.Wrapf(err, "can't connect to Bitcoin Core RPC Server %q", conf.BitcoinNode.Host)
// Initialize Bitcoin Indexer
if opts.Bitcoin {
var (
btcDB btcdatagateway.BitcoinDataGateway
indexerInfoDB btcdatagateway.IndexerInformationDataGateway
)
switch strings.ToLower(conf.Modules.Bitcoin.Database) {
case "postgresql", "postgres", "pg":
pg, err := postgres.NewPool(ctx, conf.Modules.Bitcoin.Postgres)
if err != nil {
logger.PanicContext(ctx, "Failed to create Postgres connection pool", slogx.Error(err))
}
logger.InfoContext(ctx, "Connected to Bitcoin Core RPC Server", slog.Duration("latency", time.Since(start)))
defer pg.Close()
repo := btcpostgres.NewRepository(pg)
btcDB = repo
indexerInfoDB = repo
default:
return errors.Wrapf(errs.Unsupported, "%q database is not supported", conf.Modules.Bitcoin.Database)
}
if !opts.APIOnly {
bitcoinProcessor := bitcoin.NewProcessor(conf, btcDB, indexerInfoDB)
bitcoinNodeDatasource := datasources.NewBitcoinNode(client)
bitcoinIndexer := indexers.NewBitcoinIndexer(bitcoinProcessor, bitcoinNodeDatasource)
return client, nil
})
// Initialize reporting client
do.Provide(injector, func(i do.Injector) (*reportingclient.ReportingClient, error) {
conf := do.MustInvoke[config.Config](i)
if conf.Reporting.Disabled {
return nil, nil
}
reportingClient, err := reportingclient.New(conf.Reporting)
if err != nil {
if errors.Is(err, errs.InvalidArgument) {
return nil, errors.Wrap(err, "invalid reporting configuration")
// Verify states before running Indexer
if err := bitcoinProcessor.VerifyStates(ctx); err != nil {
return errors.WithStack(err)
}
return nil, errors.Wrap(err, "can't create reporting client")
}
return reportingClient, nil
})
// Initialize HTTP server
do.Provide(injector, func(i do.Injector) (*fiber.App, error) {
// Run Indexer
go func() {
logger.InfoContext(ctx, "Starting Bitcoin Indexer")
if err := bitcoinIndexer.Run(ctx); err != nil {
logger.ErrorContext(ctx, "Failed to run Bitcoin Indexer", slogx.Error(err))
}
// stop main process if Bitcoin Indexer failed
logger.InfoContext(ctx, "Bitcoin Indexer stopped. Stopping main process...")
stop()
}()
}
}
// Initialize Runes Indexer
if opts.Runes {
var runesDg runesdatagateway.RunesDataGateway
var indexerInfoDg runesdatagateway.IndexerInfoDataGateway
switch strings.ToLower(conf.Modules.Runes.Database) {
case "postgresql", "postgres", "pg":
pg, err := postgres.NewPool(ctx, conf.Modules.Runes.Postgres)
if err != nil {
logger.PanicContext(ctx, "Failed to create Postgres connection pool", slogx.Error(err))
}
defer pg.Close()
runesRepo := runespostgres.NewRepository(pg)
runesDg = runesRepo
indexerInfoDg = runesRepo
default:
logger.PanicContext(ctx, "Unsupported database", slogx.String("database", conf.Modules.Runes.Database))
}
var bitcoinDatasource indexers.BitcoinDatasource
var bitcoinClient btcclient.Contract
switch strings.ToLower(conf.Modules.Runes.Datasource) {
case "bitcoin-node":
bitcoinNodeDatasource := datasources.NewBitcoinNode(client)
bitcoinDatasource = bitcoinNodeDatasource
bitcoinClient = bitcoinNodeDatasource
case "database":
pg, err := postgres.NewPool(ctx, conf.Modules.Runes.Postgres)
if err != nil {
logger.PanicContext(ctx, "Failed to create Postgres connection pool", slogx.Error(err))
}
defer pg.Close()
btcRepo := btcpostgres.NewRepository(pg)
btcClientDB := btcclient.NewClientDatabase(btcRepo)
bitcoinDatasource = btcClientDB
bitcoinClient = btcClientDB
default:
return errors.Wrapf(errs.Unsupported, "%q datasource is not supported", conf.Modules.Runes.Datasource)
}
if !opts.APIOnly {
runesProcessor := runes.NewProcessor(runesDg, indexerInfoDg, bitcoinClient, bitcoinDatasource, conf.Network, reportingClient)
runesIndexer := indexers.NewBitcoinIndexer(runesProcessor, bitcoinDatasource)
if err := runesProcessor.VerifyStates(ctx); err != nil {
return errors.WithStack(err)
}
// Run Indexer
go func() {
logger.InfoContext(ctx, "Started Runes Indexer")
if err := runesIndexer.Run(ctx); err != nil {
logger.ErrorContext(ctx, "Failed to run Runes Indexer", slogx.Error(err))
}
// stop main process if Runes Indexer failed
logger.InfoContext(ctx, "Runes Indexer stopped. Stopping main process...")
stop()
}()
}
// Mount API
apiHandlers := lo.Uniq(conf.Modules.Runes.APIHandlers)
for _, handler := range apiHandlers {
switch handler { // TODO: support more handlers (e.g. gRPC)
case "http":
runesUsecase := runesusecase.New(runesDg, bitcoinClient)
runesHTTPHandler := runesapi.NewHTTPHandler(conf.Network, runesUsecase)
httpHandlers["runes"] = runesHTTPHandler
default:
logger.PanicContext(ctx, "Unsupported API handler", slogx.String("handler", handler))
}
}
}
// Wait for interrupt signal to gracefully stop the server with
// Setup HTTP server if there are any HTTP handlers
if len(httpHandlers) > 0 {
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",
ErrorHandler: errorhandler.NewHTTPErrorHandler(),
})
app.
Use(favicon.New()).
Use(cors.New()).
Use(requestid.New()).
Use(requestcontext.New(
requestcontext.WithRequestId(),
requestcontext.WithClientIP(conf.HTTPServer.RequestIP),
)).
Use(requestlogger.New(conf.HTTPServer.Logger)).
Use(fiberrecover.New(fiberrecover.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
buf := make([]byte, 1024) // bufLen = 1024
buf = buf[:runtime.Stack(buf, false)]
logger.ErrorContext(c.UserContext(), "Something went wrong, panic in http handler", slogx.Any("panic", e), slog.String("stacktrace", string(buf)))
logger.ErrorContext(c.UserContext(), "panic in http handler", slogx.Any("panic", e), slog.String("stacktrace", string(buf)))
},
})).
Use(errorhandler.New()).
Use(compress.New(compress.Config{
Level: compress.LevelDefault,
}))
// Health check
app.Get("/", func(c *fiber.Ctx) error {
return errors.WithStack(c.SendStatus(http.StatusOK))
// mount http handlers from each http-enabled module
for module, handler := range httpHandlers {
if err := handler.Mount(app); err != nil {
logger.PanicContext(ctx, "Failed to mount HTTP handler", slogx.Error(err), slogx.String("module", module))
}
logger.InfoContext(ctx, "Mounted HTTP handler", slogx.String("module", module))
}
go func() {
logger.InfoContext(ctx, "Started HTTP server", slog.Int("port", conf.HTTPServer.Port))
if err := app.Listen(fmt.Sprintf(":%d", conf.HTTPServer.Port)); err != nil {
logger.PanicContext(ctx, "Failed to start HTTP server", slogx.Error(err))
}
}()
// handle graceful shutdown
gracefulEG.Go(func() error {
<-ctx.Done()
logger.InfoContext(gctx, "Stopping HTTP server...")
if err := app.ShutdownWithTimeout(60 * time.Second); err != nil {
logger.ErrorContext(gctx, "Error during shutdown HTTP server", slogx.Error(err))
}
logger.InfoContext(gctx, "HTTP server stopped gracefully")
return nil
})
return app, nil
})
// Initialize worker context to separate worker's lifecycle from main process
ctxWorker, stopWorker := context.WithCancel(context.Background())
defer stopWorker()
// Add logger context
ctxWorker = logger.WithContext(ctxWorker, slogx.Stringer("network", conf.Network))
// Run modules
{
modules := lo.Uniq(conf.EnableModules)
modules = lo.Map(modules, func(item string, _ int) string { return strings.TrimSpace(item) })
modules = lo.Filter(modules, func(item string, _ int) bool { return item != "" })
for _, module := range modules {
ctx := logger.WithContext(ctxWorker, slogx.String("module", module))
indexer, err := do.InvokeNamed[indexer.IndexerWorker](injector, module)
if err != nil {
if errors.Is(err, do.ErrServiceNotFound) {
return errors.Errorf("Module %q is not supported", module)
}
return errors.Wrapf(err, "can't init module %q", module)
}
// Run Indexer
if !conf.APIOnly {
go func() {
// stop main process if indexer stopped
defer stop()
logger.InfoContext(ctx, "Starting Gaze Indexer")
if err := indexer.Run(ctx); err != nil {
logger.PanicContext(ctx, "Something went wrong, error during running indexer", slogx.Error(err))
}
}()
}
}
}
// Run API server
httpServer := do.MustInvoke[*fiber.App](injector)
go func() {
// stop main process if API stopped
defer stop()
logger.InfoContext(ctx, "Started HTTP server", slog.Int("port", conf.HTTPServer.Port))
if err := httpServer.Listen(fmt.Sprintf(":%d", conf.HTTPServer.Port)); err != nil {
logger.PanicContext(ctx, "Something went wrong, error during running HTTP server", slogx.Error(err))
}
}()
// Stop application if worker context is done
go func() {
<-ctxWorker.Done()
defer stop()
logger.InfoContext(ctx, "Gaze Indexer Worker is stopped. Stopping application...")
}()
logger.InfoContext(ctxWorker, "Gaze Indexer started")
// Wait for interrupt signal to gracefully stop the server
<-ctx.Done()
// Force shutdown if timeout exceeded or got signal again
go func() {
defer os.Exit(1)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()
select {
case <-ctx.Done():
logger.FatalContext(ctx, "Received exit signal again. Force shutdown...")
case <-time.After(shutdownTimeout + 15*time.Second):
logger.FatalContext(ctx, "Shutdown timeout exceeded. Force shutdown...")
}
}()
if err := injector.Shutdown(); err != nil {
logger.PanicContext(ctx, "Failed while gracefully shutting down", slogx.Error(err))
// wait until all graceful shutdown goroutines are done before returning
if err := gracefulEG.Wait(); err != nil {
logger.ErrorContext(ctx, "Failed to shutdown gracefully", slogx.Error(err))
} else {
logger.InfoContext(ctx, "Successfully shut down gracefully")
}
return nil
}

View File

@@ -6,13 +6,15 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/constants"
"github.com/gaze-network/indexer-network/modules/bitcoin"
"github.com/gaze-network/indexer-network/modules/runes"
"github.com/spf13/cobra"
)
var versions = map[string]string{
"": constants.Version,
"runes": runes.Version,
"": constants.Version,
"bitcoin": bitcoin.Version,
"runes": runes.Version,
}
type versionCmdOptions struct {
@@ -31,7 +33,7 @@ func NewVersionCommand() *cobra.Command {
}
flags := cmd.Flags()
flags.StringVar(&opts.Modules, "module", "", `Show version of a specific module. E.g. "runes"`)
flags.StringVar(&opts.Modules, "module", "", `Show version of a specific module. E.g. "bitcoin" | "runes"`)
return cmd
}

View File

@@ -1,125 +0,0 @@
package migrate
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
type migrateDownCmdOptions struct {
DatabaseURL string
Runes bool
All bool
}
type migrateDownCmdArgs struct {
N int
}
func (a *migrateDownCmdArgs) ParseArgs(args []string) error {
if len(args) > 0 {
// assume args already validated by cobra to be len(args) <= 1
n, err := strconv.Atoi(args[0])
if err != nil {
return errors.Wrap(err, "failed to parse N")
}
if n < 0 {
return errors.New("N must be a positive integer")
}
a.N = n
}
return nil
}
func NewMigrateDownCommand() *cobra.Command {
opts := &migrateDownCmdOptions{}
cmd := &cobra.Command{
Use: "down [N]",
Short: "Apply all or N down migrations",
Args: cobra.MaximumNArgs(1),
Example: `gaze migrate down --database "postgres://postgres:postgres@localhost:5432/gaze-indexer?sslmode=disable"`,
RunE: func(cmd *cobra.Command, args []string) error {
// args already validated by cobra
var downArgs migrateDownCmdArgs
if err := downArgs.ParseArgs(args); err != nil {
return errors.Wrap(err, "failed to parse args")
}
return migrateDownHandler(opts, cmd, downArgs)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes down migrations")
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
flags.BoolVar(&opts.All, "all", false, "Confirm apply ALL down migrations without prompt")
return cmd
}
func migrateDownHandler(opts *migrateDownCmdOptions, _ *cobra.Command, args migrateDownCmdArgs) error {
if opts.DatabaseURL == "" {
return errors.New("--database is required")
}
databaseURL, err := url.Parse(opts.DatabaseURL)
if err != nil {
return errors.Wrap(err, "failed to parse database URL")
}
if _, ok := supportedDrivers[databaseURL.Scheme]; !ok {
return errors.Errorf("unsupported database driver: %s", databaseURL.Scheme)
}
// prevent accidental down all migrations
if args.N == 0 && !opts.All {
input := ""
fmt.Print("Are you sure you want to apply all down migrations? (y/N):")
fmt.Scanln(&input)
if !lo.Contains([]string{"y", "yes"}, strings.ToLower(input)) {
return nil
}
}
applyDownMigrations := func(module string, sourcePath string, migrationTable string) error {
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
sourceURL := "file://" + sourcePath
m, err := migrate.New(sourceURL, newDatabaseURL.String())
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return errors.Wrap(errs.InternalError, "migrations directory not found")
}
return errors.Wrap(err, "failed to open database")
}
m.Log = &consoleLogger{
prefix: fmt.Sprintf("[%s] ", module),
}
if args.N == 0 {
m.Log.Printf("Applying down migrations...\n")
err = m.Down()
} else {
m.Log.Printf("Applying %d down migrations...\n", args.N)
err = m.Steps(-args.N)
}
if err != nil {
if !errors.Is(err, migrate.ErrNoChange) {
return errors.Wrapf(err, "failed to apply %s down migrations", module)
}
m.Log.Printf("No more down migrations to apply\n")
}
return nil
}
if opts.Runes {
if err := applyDownMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@@ -1,110 +0,0 @@
package migrate
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/spf13/cobra"
)
type migrateUpCmdOptions struct {
DatabaseURL string
Runes bool
}
type migrateUpCmdArgs struct {
N int
}
func (a *migrateUpCmdArgs) ParseArgs(args []string) error {
if len(args) > 0 {
// assume args already validated by cobra to be len(args) <= 1
n, err := strconv.Atoi(args[0])
if err != nil {
return errors.Wrap(err, "failed to parse N")
}
a.N = n
}
return nil
}
func NewMigrateUpCommand() *cobra.Command {
opts := &migrateUpCmdOptions{}
cmd := &cobra.Command{
Use: "up [N]",
Short: "Apply all or N up migrations",
Args: cobra.MaximumNArgs(1),
Example: `gaze migrate up --database "postgres://postgres:postgres@localhost:5432/gaze-indexer?sslmode=disable"`,
RunE: func(cmd *cobra.Command, args []string) error {
// args already validated by cobra
var upArgs migrateUpCmdArgs
if err := upArgs.ParseArgs(args); err != nil {
return errors.Wrap(err, "failed to parse args")
}
return migrateUpHandler(opts, cmd, upArgs)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes up migrations")
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
return cmd
}
func migrateUpHandler(opts *migrateUpCmdOptions, _ *cobra.Command, args migrateUpCmdArgs) error {
if opts.DatabaseURL == "" {
return errors.New("--database is required")
}
databaseURL, err := url.Parse(opts.DatabaseURL)
if err != nil {
return errors.Wrap(err, "failed to parse database URL")
}
if _, ok := supportedDrivers[databaseURL.Scheme]; !ok {
return errors.Errorf("unsupported database driver: %s", databaseURL.Scheme)
}
applyUpMigrations := func(module string, sourcePath string, migrationTable string) error {
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
sourceURL := "file://" + sourcePath
m, err := migrate.New(sourceURL, newDatabaseURL.String())
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return errors.Wrap(errs.InternalError, "migrations directory not found")
}
return errors.Wrap(err, "failed to open database")
}
m.Log = &consoleLogger{
prefix: fmt.Sprintf("[%s] ", module),
}
if args.N == 0 {
m.Log.Printf("Applying up migrations...\n")
err = m.Up()
} else {
m.Log.Printf("Applying %d up migrations...\n", args.N)
err = m.Steps(args.N)
}
if err != nil {
if !errors.Is(err, migrate.ErrNoChange) {
return errors.Wrapf(err, "failed to apply %s up migrations", module)
}
m.Log.Printf("Migrations already up-to-date\n")
}
return nil
}
if opts.Runes {
if err := applyUpMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@@ -1,22 +0,0 @@
package migrate
import (
"fmt"
"github.com/golang-migrate/migrate/v4"
)
var _ migrate.Logger = (*consoleLogger)(nil)
type consoleLogger struct {
prefix string
verbose bool
}
func (l *consoleLogger) Printf(format string, v ...interface{}) {
fmt.Printf(l.prefix+format, v...)
}
func (l *consoleLogger) Verbose() bool {
return l.verbose
}

View File

@@ -1,24 +0,0 @@
package migrate
import "net/url"
const (
runesMigrationSource = "modules/runes/database/postgresql/migrations"
)
func cloneURLWithQuery(u *url.URL, newQuery url.Values) *url.URL {
clone := *u
query := clone.Query()
for key, values := range newQuery {
for _, value := range values {
query.Add(key, value)
}
}
clone.RawQuery = query.Encode()
return &clone
}
var supportedDrivers = map[string]struct{}{
"postgres": {},
"postgresql": {},
}

View File

@@ -1,44 +1,39 @@
logger:
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
output: text
debug: false
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
bitcoin_node:
host: "bitcoin-mainnet-archive.allthatnode.com"
user: "user"
pass: "pass"
disable_tls: false
network: mainnet
# Bitcoin Core RPC configuration options.
bitcoin_node:
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
user: "" # Username to authenticate with Bitcoin Core RPC
pass: "" # Password to authenticate with Bitcoin Core RPC
disable_tls: false # Set to true to disable tls
# Block reporting configuration options. See Block Reporting section for more details.
reporting:
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
disabled: false
base_url: "https://indexer.api.gaze.network" # defaults to "https://indexer.api.gaze.network" if empty
name: "local-dev" # name of this instance to show on the dashboard
website_url: "" # public website URL to show on the dashboard. Can be left empty.
indexer_api_url: "" # public url to access this api. Can be left empty.
# HTTP server configuration options.
http_server:
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
logger:
disable: false # disable logger if logger level is `INFO`
request_header: false
request_query: false
requestip: # Client IP extraction configuration options. This is unnecessary if you don't care about the real client IP or if you're not using a reverse proxy.
trusted_proxies_ip: # Cloudflare, GCP Public LB. See: server/internal/middleware/requestcontext/PROXY-IP.md
trusted_proxies_header: # X-Real-IP, CF-Connecting-IP
enable_reject_malformed_request: false # return 403 if request is malformed (invalid IP)
้http_server:
port: 8080
# Meta-protocol modules configuration options.
modules:
# Configuration options for Runes module. Can be removed if not used.
bitcoin:
database: "postgres" # Database to store bitcoin data. current supported databases: "postgres"
postgres:
host: "localhost"
port: 5432
user: "postgres"
password: "password"
db_name: "postgres"
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database connection configurations
runes:
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
datasource: "database" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
api_handlers: # API handlers to enable. current supported handlers: "http"
datasource: "bitcoin-node" # Data source to be used. current supported data sources: "bitcoin-node" | "postgres"
api_handlers: # API handlers to be used. current supported handlers: "http"
- http
postgres:
host: "localhost"
@@ -46,4 +41,4 @@ modules:
user: "postgres"
password: "password"
db_name: "postgres"
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database connection configurations

View File

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

View File

@@ -24,7 +24,7 @@ const (
)
// Make sure to implement the BitcoinDatasource interface
var _ Datasource[*types.Block] = (*BitcoinNodeDatasource)(nil)
var _ Datasource[[]*types.Block] = (*BitcoinNodeDatasource)(nil)
// BitcoinNodeDatasource fetch data from Bitcoin node for Bitcoin Indexer
type BitcoinNodeDatasource struct {
@@ -39,7 +39,7 @@ func NewBitcoinNode(btcclient *rpcclient.Client) *BitcoinNodeDatasource {
}
func (p BitcoinNodeDatasource) Name() string {
return "bitcoin_node"
return "BitcoinNode"
}
// Fetch polling blocks from Bitcoin node
@@ -83,11 +83,6 @@ func (d *BitcoinNodeDatasource) Fetch(ctx context.Context, from, to int64) ([]*t
// - from: block height to start fetching, if -1, it will start from genesis block
// - to: block height to stop fetching, if -1, it will fetch until the latest block
func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64, ch chan<- []*types.Block) (*subscription.ClientSubscription[[]*types.Block], error) {
ctx = logger.WithContext(ctx,
slogx.String("package", "datasources"),
slogx.String("datasource", d.Name()),
)
from, to, skip, err := d.prepareRange(from, to)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare fetch range")
@@ -143,10 +138,10 @@ func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64,
if errors.Is(err, errs.Closed) {
return
}
logger.WarnContext(ctx, "Failed to send bitcoin blocks to subscription client",
logger.WarnContext(ctx, "failed while dispatch block",
slogx.Error(err),
slogx.Int64("start", data[0].Header.Height),
slogx.Int64("end", data[len(data)-1].Header.Height),
slogx.Error(err),
)
}
case <-ctx.Done():
@@ -162,7 +157,6 @@ func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64,
done := subscription.Done()
chunks := lo.Chunk(blockHeights, blockStreamChunkSize)
for _, chunk := range chunks {
// TODO: Implement throttling logic to control the rate of fetching blocks (block/sec)
chunk := chunk
select {
case <-done:
@@ -173,11 +167,12 @@ func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64,
stream.Go(func() []*types.Block {
startAt := time.Now()
defer func() {
logger.DebugContext(ctx, "Fetched chunk of blocks from Bitcoin node",
logger.DebugContext(ctx, "[BitcoinNodeDatasource] Fetched blocks",
slogx.Int("total_blocks", len(chunk)),
slogx.Int64("from", chunk[0]),
slogx.Int64("to", chunk[len(chunk)-1]),
slogx.Duration("duration", time.Since(startAt)),
slogx.Stringer("duration", time.Since(startAt)),
slogx.Int64("duration_ms", time.Since(startAt).Milliseconds()),
)
}()
// TODO: should concurrent fetch block or not ?
@@ -185,21 +180,22 @@ func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64,
for _, height := range chunk {
hash, err := d.btcclient.GetBlockHash(height)
if err != nil {
logger.ErrorContext(ctx, "Can't get block hash from Bitcoin node rpc", slogx.Error(err), slogx.Int64("height", height))
logger.ErrorContext(ctx, "failed to get block hash", slogx.Error(err), slogx.Int64("height", height))
if err := subscription.SendError(ctx, errors.Wrapf(err, "failed to get block hash: height: %d", height)); err != nil {
logger.WarnContext(ctx, "Failed to send datasource error to subscription client", slogx.Error(err))
logger.ErrorContext(ctx, "failed to send error", slogx.Error(err))
}
return nil
}
block, err := d.btcclient.GetBlock(hash)
if err != nil {
logger.ErrorContext(ctx, "Can't get block data from Bitcoin node rpc", slogx.Error(err), slogx.Int64("height", height))
logger.ErrorContext(ctx, "failed to get block", slogx.Error(err), slogx.Int64("height", height))
if err := subscription.SendError(ctx, errors.Wrapf(err, "failed to get block: height: %d, hash: %s", height, hash)); err != nil {
logger.WarnContext(ctx, "Failed to send datasource error to subscription client", slogx.Error(err))
logger.ErrorContext(ctx, "failed to send error", slogx.Error(err))
}
return nil
}
logger.DebugContext(ctx, "[BitcoinNodeDatasource] Fetched block", slogx.Int64("height", height), slogx.String("hash", hash.String()))
blocks = append(blocks, types.ParseMsgBlock(block, height))
}
@@ -243,32 +239,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

View File

@@ -10,7 +10,7 @@ import (
// Datasource is an interface for indexer data sources.
type Datasource[T any] interface {
Name() string
Fetch(ctx context.Context, from, to int64) ([]T, error)
FetchAsync(ctx context.Context, from, to int64, ch chan<- []T) (*subscription.ClientSubscription[[]T], error)
Fetch(ctx context.Context, from, to int64) (T, error)
FetchAsync(ctx context.Context, from, to int64, ch chan<- T) (*subscription.ClientSubscription[T], error)
GetBlockHeader(ctx context.Context, height int64) (types.BlockHeader, error)
}

View File

@@ -1,257 +0,0 @@
package indexer
import (
"context"
"log/slog"
"sync"
"time"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
)
const (
maxReorgLookBack = 1000
// pollingInterval is the default polling interval for the indexer polling worker
pollingInterval = 15 * time.Second
)
// Indexer generic indexer for fetching and processing data
type Indexer[T Input] struct {
Processor Processor[T]
Datasource datasources.Datasource[T]
currentBlock types.BlockHeader
quitOnce sync.Once
quit chan struct{}
done chan struct{}
}
// New create new generic indexer
func New[T Input](processor Processor[T], datasource datasources.Datasource[T]) *Indexer[T] {
return &Indexer[T]{
Processor: processor,
Datasource: datasource,
quit: make(chan struct{}),
done: make(chan struct{}),
}
}
func (i *Indexer[T]) Shutdown() error {
return i.ShutdownWithContext(context.Background())
}
func (i *Indexer[T]) ShutdownWithTimeout(timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return i.ShutdownWithContext(ctx)
}
func (i *Indexer[T]) ShutdownWithContext(ctx context.Context) (err error) {
i.quitOnce.Do(func() {
close(i.quit)
select {
case <-i.done:
case <-time.After(180 * time.Second):
err = errors.Wrap(errs.Timeout, "indexer shutdown timeout")
case <-ctx.Done():
err = errors.Wrap(ctx.Err(), "indexer shutdown context canceled")
}
})
return
}
func (i *Indexer[T]) Run(ctx context.Context) (err error) {
defer close(i.done)
ctx = logger.WithContext(ctx,
slog.String("package", "indexers"),
slog.String("processor", i.Processor.Name()),
slog.String("datasource", i.Datasource.Name()),
)
// set to -1 to start from genesis block
i.currentBlock, err = i.Processor.CurrentBlock(ctx)
if err != nil {
if !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "can't init state, failed to get indexer current block")
}
i.currentBlock.Height = -1
}
ticker := time.NewTicker(pollingInterval)
defer ticker.Stop()
for {
select {
case <-i.quit:
logger.InfoContext(ctx, "Got quit signal, stopping indexer")
if err := i.Processor.Shutdown(ctx); err != nil {
logger.ErrorContext(ctx, "Failed to shutdown processor", slogx.Error(err))
return errors.Wrap(err, "processor shutdown failed")
}
return nil
case <-ctx.Done():
return nil
case <-ticker.C:
if err := i.process(ctx); err != nil {
logger.ErrorContext(ctx, "Indexer failed while processing", slogx.Error(err))
return errors.Wrap(err, "process failed")
}
logger.DebugContext(ctx, "Waiting for next polling interval")
}
}
}
func (i *Indexer[T]) process(ctx context.Context) (err error) {
// height range to fetch data
from, to := i.currentBlock.Height+1, int64(-1)
logger.InfoContext(ctx, "Start fetching input data", slog.Int64("from", from))
ch := make(chan []T)
subscription, err := i.Datasource.FetchAsync(ctx, from, to, ch)
if err != nil {
return errors.Wrap(err, "failed to fetch input data")
}
defer subscription.Unsubscribe()
for {
select {
case <-i.quit:
return nil
case inputs := <-ch:
// empty inputs
if len(inputs) == 0 {
continue
}
firstInput := inputs[0]
firstInputHeader := firstInput.BlockHeader()
startAt := time.Now()
ctx := logger.WithContext(ctx,
slogx.Int64("from", firstInputHeader.Height),
slogx.Int64("to", inputs[len(inputs)-1].BlockHeader().Height),
)
// validate reorg from first input
{
remoteBlockHeader := firstInputHeader
if !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
logger.WarnContext(ctx, "Detected chain reorganization. Searching for fork point...",
slogx.String("event", "reorg_detected"),
slogx.Stringer("current_hash", i.currentBlock.Hash),
slogx.Stringer("expected_hash", remoteBlockHeader.PrevBlock),
)
var (
start = time.Now()
targetHeight = i.currentBlock.Height - 1
beforeReorgBlockHeader = types.BlockHeader{
Height: -1,
}
)
for n := 0; n < maxReorgLookBack; n++ {
// TODO: concurrent fetch
indexedHeader, err := i.Processor.GetIndexedBlock(ctx, targetHeight)
if err != nil {
return errors.Wrapf(err, "failed to get indexed block, height: %d", targetHeight)
}
remoteHeader, err := i.Datasource.GetBlockHeader(ctx, targetHeight)
if err != nil {
return errors.Wrapf(err, "failed to get remote block header, height: %d", targetHeight)
}
// Found no reorg block
if indexedHeader.Hash.IsEqual(&remoteHeader.Hash) {
beforeReorgBlockHeader = remoteHeader
break
}
// Walk back to find fork point
targetHeight -= 1
}
// Reorg look back limit reached
if beforeReorgBlockHeader.Height < 0 {
return errors.Wrap(errs.SomethingWentWrong, "reorg look back limit reached")
}
logger.InfoContext(ctx, "Found reorg fork point, starting to revert data...",
slogx.String("event", "reorg_forkpoint"),
slogx.Int64("since", beforeReorgBlockHeader.Height+1),
slogx.Int64("total_blocks", i.currentBlock.Height-beforeReorgBlockHeader.Height),
slogx.Duration("search_duration", time.Since(start)),
)
// Revert all data since the reorg block
start = time.Now()
if err := i.Processor.RevertData(ctx, beforeReorgBlockHeader.Height+1); err != nil {
return errors.Wrap(err, "failed to revert data")
}
// Set current block to before reorg block and
// end current round to fetch again
i.currentBlock = beforeReorgBlockHeader
logger.Info("Fixing chain reorganization completed",
slogx.Int64("current_block", i.currentBlock.Height),
slogx.Duration("duration", time.Since(start)),
)
return nil
}
}
// validate is input is continuous and no reorg
prevHeader := i.currentBlock
for i, input := range inputs {
header := input.BlockHeader()
if header.Height != prevHeader.Height+1 {
return errors.Wrapf(errs.InternalError, "input is not continuous, input[%d] height: %d, input[%d] height: %d", i-1, prevHeader.Height, i, header.Height)
}
if !header.PrevBlock.IsEqual(&prevHeader.Hash) {
logger.WarnContext(ctx, "Chain Reorganization occurred in the middle of batch fetching inputs, need to try to fetch again")
// end current round
return nil
}
prevHeader = header
}
ctx = logger.WithContext(ctx, slog.Int("total_inputs", len(inputs)))
// Start processing input
logger.InfoContext(ctx, "Processing inputs")
if err := i.Processor.Process(ctx, inputs); err != nil {
return errors.WithStack(err)
}
// Update current state
i.currentBlock = inputs[len(inputs)-1].BlockHeader()
logger.InfoContext(ctx, "Processed inputs successfully",
slogx.String("event", "processed_inputs"),
slogx.Int64("current_block", i.currentBlock.Height),
slogx.Duration("duration", time.Since(startAt)),
)
case <-subscription.Done():
// end current round
if err := ctx.Err(); err != nil {
return errors.Wrap(err, "context done")
}
return nil
case <-ctx.Done():
return errors.WithStack(ctx.Err())
case err := <-subscription.Err():
if err != nil {
return errors.Wrap(err, "got error while fetch async")
}
}
}
}

View File

@@ -0,0 +1,209 @@
package indexers
import (
"context"
"log/slog"
"time"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
)
const (
maxReorgLookBack = 1000
)
type (
BitcoinProcessor Processor[[]*types.Block]
BitcoinDatasource datasources.Datasource[[]*types.Block]
)
// Make sure to implement the IndexerWorker interface
var _ IndexerWorker = (*BitcoinIndexer)(nil)
// BitcoinIndexer is the polling indexer for sync Bitcoin data to the database.
type BitcoinIndexer struct {
Processor BitcoinProcessor
Datasource BitcoinDatasource
currentBlock types.BlockHeader
}
// NewBitcoinIndexer create new BitcoinIndexer
func NewBitcoinIndexer(processor BitcoinProcessor, datasource BitcoinDatasource) *BitcoinIndexer {
return &BitcoinIndexer{
Processor: processor,
Datasource: datasource,
}
}
func (i *BitcoinIndexer) Run(ctx context.Context) (err error) {
ctx = logger.WithContext(ctx,
slog.String("indexer", "bitcoin"),
slog.String("processor", i.Processor.Name()),
slog.String("datasource", i.Datasource.Name()),
)
// set to -1 to start from genesis block
i.currentBlock, err = i.Processor.CurrentBlock(ctx)
if err != nil {
if !errors.Is(err, errs.NotFound) {
return errors.Wrap(err, "can't init state, failed to get indexer current block")
}
i.currentBlock.Height = -1
}
// TODO:
// - compare db version in constants and database
// - compare current network and local indexed network
// - update indexer stats
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if err := i.process(ctx); err != nil {
logger.ErrorContext(ctx, "failed to process", slogx.Error(err))
return errors.Wrap(err, "failed to process")
}
}
}
}
func (i *BitcoinIndexer) process(ctx context.Context) (err error) {
ch := make(chan []*types.Block)
from, to := i.currentBlock.Height+1, int64(-1)
logger.InfoContext(ctx, "Fetching blocks", slog.Int64("from", from), slog.Int64("to", to))
subscription, err := i.Datasource.FetchAsync(ctx, from, to, ch)
if err != nil {
return errors.Wrap(err, "failed to call fetch async")
}
defer subscription.Unsubscribe()
for {
select {
case blocks := <-ch:
// empty blocks
if len(blocks) == 0 {
continue
}
startAt := time.Now()
ctx := logger.WithContext(ctx,
slog.Int("total_blocks", len(blocks)),
slogx.Int64("from", blocks[0].Header.Height),
slogx.Int64("to", blocks[len(blocks)-1].Header.Height),
)
// validate reorg from first block
{
remoteBlockHeader := blocks[0].Header
if !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
logger.WarnContext(ctx, "Reorg detected",
slogx.Stringer("current_hash", i.currentBlock.Hash),
slogx.Stringer("expected_hash", remoteBlockHeader.PrevBlock),
)
var (
start = time.Now()
targetHeight = i.currentBlock.Height - 1
beforeReorgBlockHeader = types.BlockHeader{
Height: -1,
}
)
for n := 0; n < maxReorgLookBack; n++ {
// TODO: concurrent fetch
indexedHeader, err := i.Processor.GetIndexedBlock(ctx, targetHeight)
if err != nil {
return errors.Wrapf(err, "failed to get indexed block, height: %d", targetHeight)
}
remoteHeader, err := i.Datasource.GetBlockHeader(ctx, targetHeight)
if err != nil {
return errors.Wrapf(err, "failed to get remote block header, height: %d", targetHeight)
}
// Found no reorg block
if indexedHeader.Hash.IsEqual(&remoteHeader.Hash) {
beforeReorgBlockHeader = remoteHeader
break
}
// Walk back to find fork point
targetHeight -= 1
}
// Reorg look back limit reached
if beforeReorgBlockHeader.Height < 0 {
return errors.Wrap(errs.SomethingWentWrong, "reorg look back limit reached")
}
// Revert all data since the reorg block
logger.WarnContext(ctx, "reverting reorg data",
slogx.Int64("reorg_from", beforeReorgBlockHeader.Height+1),
slogx.Int64("total_reorg_blocks", i.currentBlock.Height-beforeReorgBlockHeader.Height),
slogx.Stringer("detect_duration", time.Since(start)),
)
if err := i.Processor.RevertData(ctx, beforeReorgBlockHeader.Height+1); err != nil {
return errors.Wrap(err, "failed to revert data")
}
// Set current block to before reorg block and
// end current round to fetch again
i.currentBlock = beforeReorgBlockHeader
logger.Info("Reverted data successfully",
slogx.Any("current_block", i.currentBlock),
slogx.Stringer("duration", time.Since(start)),
slogx.Int64("duration_ms", time.Since(start).Milliseconds()),
)
return nil
}
}
// validate is block is continuous and no reorg
for i := 1; i < len(blocks); i++ {
if blocks[i].Header.Height != blocks[i-1].Header.Height+1 {
return errors.Wrapf(errs.InternalError, "block is not continuous, block[%d] height: %d, block[%d] height: %d", i-1, blocks[i-1].Header.Height, i, blocks[i].Header.Height)
}
if !blocks[i].Header.PrevBlock.IsEqual(&blocks[i-1].Header.Hash) {
logger.WarnContext(ctx, "reorg occurred while batch fetching blocks, need to try to fetch again")
// end current round
return nil
}
}
// Start processing blocks
logger.InfoContext(ctx, "Processing blocks")
if err := i.Processor.Process(ctx, blocks); err != nil {
return errors.WithStack(err)
}
// Update current state
i.currentBlock = blocks[len(blocks)-1].Header
logger.InfoContext(ctx, "Processed blocks successfully",
slogx.Stringer("duration", time.Since(startAt)),
slogx.Int64("duration_ms", time.Since(startAt).Milliseconds()),
)
case <-subscription.Done():
// end current round
if err := ctx.Err(); err != nil {
return errors.Wrap(err, "context done")
}
return nil
case <-ctx.Done():
return errors.WithStack(ctx.Err())
case err := <-subscription.Err():
if err != nil {
return errors.Wrap(err, "got error while fetch async")
}
}
}
}

View File

@@ -1,21 +1,20 @@
package indexer
package indexers
import (
"context"
"time"
"github.com/gaze-network/indexer-network/core/types"
)
type Input interface {
BlockHeader() types.BlockHeader
type IndexerWorker interface {
Run(ctx context.Context) error
}
type Processor[T Input] interface {
type Processor[T any] interface {
Name() string
// Process processes the input data and indexes it.
Process(ctx context.Context, inputs []T) error
Process(ctx context.Context, inputs T) error
// CurrentBlock returns the latest indexed block header.
CurrentBlock(ctx context.Context) (types.BlockHeader, error)
@@ -29,14 +28,4 @@ type Processor[T Input] interface {
// VerifyStates verifies the states of the indexed data and the indexer
// to ensure the last shutdown was graceful and no missing data.
VerifyStates(ctx context.Context) error
// Shutdown gracefully stops the processor. Database connections, network calls, leftover states, etc. should be closed and cleaned up here.
Shutdown(ctx context.Context) error
}
type IndexerWorker interface {
Shutdown() error
ShutdownWithTimeout(timeout time.Duration) error
ShutdownWithContext(ctx context.Context) (err error)
Run(ctx context.Context) (err error)
}

View File

@@ -38,10 +38,6 @@ type Block struct {
Transactions []*Transaction
}
func (b *Block) BlockHeader() BlockHeader {
return b.Header
}
func ParseMsgBlock(src *wire.MsgBlock, height int64) *Block {
hash := src.Header.BlockHash()
return &Block{

30
go.mod
View File

@@ -6,31 +6,27 @@ require (
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11
github.com/btcsuite/btcd v0.24.0
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/cockroachdb/errors v1.11.1
github.com/gaze-network/uint128 v1.3.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v5 v5.5.5
github.com/mcosta74/pgx-slog v0.3.0
github.com/planxnx/concurrent-stream v0.1.5
github.com/samber/do/v2 v2.0.0-beta.7
github.com/samber/lo v1.39.0
github.com/shopspring/decimal v1.3.1
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/valyala/fasthttp v1.51.0
github.com/stretchr/testify v1.8.4
go.uber.org/automaxprocs v1.5.3
golang.org/x/sync v0.7.0
golang.org/x/sync v0.5.0
)
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 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
@@ -38,23 +34,21 @@ require (
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/ecies/go/v2 v2.0.9 // indirect
github.com/ethereum/go-ethereum v1.13.5 // 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
github.com/google/uuid v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx v3.6.2+incompatible // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -67,19 +61,19 @@ require (
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/samber/go-type-to-string v1.4.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
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.17.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.15.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
)

1133
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -8,49 +8,41 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
btcconfig "github.com/gaze-network/indexer-network/modules/bitcoin/config"
runesconfig "github.com/gaze-network/indexer-network/modules/runes/config"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
"github.com/gaze-network/indexer-network/pkg/middleware/requestlogger"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gaze-network/indexer-network/pkg/reportingclientv2"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
var (
isInit bool
mu sync.Mutex
config = &Config{
configOnce sync.Once
config = &Config{
Logger: logger.Config{
Output: "TEXT",
},
Network: common.NetworkMainnet,
HTTPServer: HTTPServerConfig{
Port: 8080,
},
BitcoinNode: BitcoinNodeClient{
User: "user",
Pass: "pass",
},
Modules: Modules{
Runes: runesconfig.Config{
Datasource: "bitcoin-node",
Database: "postgres",
},
},
}
)
type Config struct {
EnableModules []string `mapstructure:"enable_modules"`
APIOnly bool `mapstructure:"api_only"`
Logger logger.Config `mapstructure:"logger"`
BitcoinNode BitcoinNodeClient `mapstructure:"bitcoin_node"`
Network common.Network `mapstructure:"network"`
HTTPServer HTTPServerConfig `mapstructure:"http_server"`
Modules Modules `mapstructure:"modules"`
Reporting reportingclient.Config `mapstructure:"reporting"`
Logger logger.Config `mapstructure:"logger"`
BitcoinNode BitcoinNodeClient `mapstructure:"bitcoin_node"`
Network common.Network `mapstructure:"network"`
HTTPServer HTTPServerConfig `mapstructure:"http_server"`
Modules Modules `mapstructure:"modules"`
Reporting reportingclientv2.Config `mapstructure:"reporting"`
NodeKey NodeKey `mapstructure:"node_key"`
}
type NodeKey struct {
Path string `mapstructure:"path"`
}
type BitcoinNodeClient struct {
@@ -61,49 +53,16 @@ type BitcoinNodeClient struct {
}
type Modules struct {
Runes runesconfig.Config `mapstructure:"runes"`
Bitcoin btcconfig.Config `mapstructure:"bitcoin"`
Runes runesconfig.Config `mapstructure:"runes"`
}
type HTTPServerConfig struct {
Port int `mapstructure:"port"`
Logger requestlogger.Config `mapstructure:"logger"`
RequestIP requestcontext.WithClientIPConfig `mapstructure:"requestip"`
Port int `mapstructure:"port"`
}
// Parse parse the configuration from environment variables
func Parse(configFile ...string) Config {
mu.Lock()
defer mu.Unlock()
return parse(configFile...)
}
// Load returns the loaded configuration
func Load() Config {
mu.Lock()
defer mu.Unlock()
if isInit {
return *config
}
return parse()
}
// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func BindPFlag(key string, flag *pflag.Flag) {
if err := viper.BindPFlag(key, flag); err != nil {
logger.Panic("Something went wrong, failed to bind flag for config", slog.String("package", "config"), slogx.Error(err))
}
}
// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func SetDefault(key string, value any) { viper.SetDefault(key, value) }
func parse(configFile ...string) Config {
ctx := logger.WithContext(context.Background(), slog.String("package", "config"))
if len(configFile) > 0 && configFile[0] != "" {
@@ -118,16 +77,39 @@ func parse(configFile ...string) Config {
if err := viper.ReadInConfig(); err != nil {
var errNotfound viper.ConfigFileNotFoundError
if errors.As(err, &errNotfound) {
logger.WarnContext(ctx, "Config file not found, use default config value", slogx.Error(err))
logger.WarnContext(ctx, "config file not found, use default value", slogx.Error(err))
} else {
logger.PanicContext(ctx, "Invalid config file", slogx.Error(err))
logger.PanicContext(ctx, "invalid config file", slogx.Error(err))
}
}
if err := viper.Unmarshal(&config); err != nil {
logger.PanicContext(ctx, "Something went wrong, failed to unmarshal config", slogx.Error(err))
logger.PanicContext(ctx, "failed to unmarshal config", slogx.Error(err))
}
isInit = true
return *config
}
// Load returns the loaded configuration
func Load() Config {
configOnce.Do(func() {
_ = Parse()
})
return *config
}
// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func BindPFlag(key string, flag *pflag.Flag) {
if err := viper.BindPFlag(key, flag); err != nil {
logger.Panic("Failed to bind pflag for config", slogx.Error(err))
}
}
// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func SetDefault(key string, value any) { viper.SetDefault(key, value) }

View File

@@ -6,7 +6,6 @@ import (
"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/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
@@ -40,7 +39,7 @@ func New(ctx context.Context, conf Config) (*pgx.Conn, error) {
// Prepare connection pool configuration
connConfig, err := pgx.ParseConfig(conf.String())
if err != nil {
return nil, errors.Join(errs.InvalidArgument, errors.Wrap(err, "failed while parse config"))
return nil, errors.Wrap(err, "failed to parse config to create a new connection")
}
connConfig.Tracer = conf.QueryTracer()
@@ -63,7 +62,7 @@ func NewPool(ctx context.Context, conf Config) (*pgxpool.Pool, error) {
// Prepare connection pool configuration
connConfig, err := pgxpool.ParseConfig(conf.String())
if err != nil {
return nil, errors.Join(errs.InvalidArgument, errors.Wrap(err, "failed while parse config"))
return nil, errors.Wrap(err, "failed to parse config to create a new connection pool")
}
connConfig.MaxConns = utils.Default(conf.MaxConns, DefaultMaxConns)
connConfig.MinConns = utils.Default(conf.MinConns, DefaultMinConns)

View File

@@ -7,6 +7,7 @@ import (
"syscall"
"github.com/gaze-network/indexer-network/cmd"
_ "go.uber.org/automaxprocs"
)
func main() {

View File

@@ -0,0 +1,229 @@
package btcclient
import (
"context"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/internal/subscription"
"github.com/gaze-network/indexer-network/modules/bitcoin/datagateway"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
cstream "github.com/planxnx/concurrent-stream"
"github.com/samber/lo"
)
// TODO: Refactor this, datasources.BitcoinNode and This package is the same.
const (
blockStreamChunkSize = 100
)
// Make sure to implement the BitcoinDatasource interface
var _ datasources.Datasource[[]*types.Block] = (*ClientDatabase)(nil)
// ClientDatabase is a client to connect to the bitcoin database.
type ClientDatabase struct {
bitcoinDg datagateway.BitcoinDataGateway
}
func NewClientDatabase(bitcoinDg datagateway.BitcoinDataGateway) *ClientDatabase {
return &ClientDatabase{
bitcoinDg: bitcoinDg,
}
}
func (c ClientDatabase) Name() string {
return "BitcoinDatabase"
}
func (c *ClientDatabase) Fetch(ctx context.Context, from, to int64) ([]*types.Block, error) {
ch := make(chan []*types.Block)
subscription, err := c.FetchAsync(ctx, from, to, ch)
if err != nil {
return nil, errors.WithStack(err)
}
defer subscription.Unsubscribe()
blocks := make([]*types.Block, 0)
for {
select {
case b, ok := <-ch:
if !ok {
return blocks, nil
}
blocks = append(blocks, b...)
case <-subscription.Done():
if err := ctx.Err(); err != nil {
return nil, errors.Wrap(err, "context done")
}
return blocks, nil
case err := <-subscription.Err():
if err != nil {
return nil, errors.Wrap(err, "got error while fetch async")
}
return blocks, nil
case <-ctx.Done():
return nil, errors.Wrap(ctx.Err(), "context done")
}
}
}
func (c *ClientDatabase) FetchAsync(ctx context.Context, from, to int64, ch chan<- []*types.Block) (*subscription.ClientSubscription[[]*types.Block], error) {
from, to, skip, err := c.prepareRange(ctx, from, to)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare fetch range")
}
subscription := subscription.NewSubscription(ch)
if skip {
if err := subscription.UnsubscribeWithContext(ctx); err != nil {
return nil, errors.Wrap(err, "failed to unsubscribe")
}
return subscription.Client(), nil
}
// Create parallel stream
out := make(chan []*types.Block)
stream := cstream.NewStream(ctx, 8, out)
// create slice of block height to fetch
blockHeights := make([]int64, 0, to-from+1)
for i := from; i <= to; i++ {
blockHeights = append(blockHeights, i)
}
// Wait for stream to finish and close out channel
go func() {
defer close(out)
_ = stream.Wait()
}()
// Fan-out blocks to subscription channel
go func() {
defer func() {
// add a bit delay to prevent shutdown before client receive all blocks
time.Sleep(100 * time.Millisecond)
subscription.Unsubscribe()
}()
for {
select {
case data, ok := <-out:
// stream closed
if !ok {
return
}
// empty blocks
if len(data) == 0 {
continue
}
// send blocks to subscription channel
if err := subscription.Send(ctx, data); err != nil {
if errors.Is(err, errs.Closed) {
return
}
logger.WarnContext(ctx, "failed while dispatch block",
slogx.Error(err),
slogx.Int64("start", data[0].Header.Height),
slogx.Int64("end", data[len(data)-1].Header.Height),
)
}
case <-ctx.Done():
return
}
}
}()
// Parallel fetch blocks from Bitcoin node until complete all block heights
// or subscription is done.
go func() {
defer stream.Close()
done := subscription.Done()
chunks := lo.Chunk(blockHeights, blockStreamChunkSize)
for _, chunk := range chunks {
chunk := chunk
select {
case <-done:
return
case <-ctx.Done():
return
default:
if len(chunk) == 0 {
continue
}
stream.Go(func() []*types.Block {
fromHeight, toHeight := chunk[0], chunk[len(chunk)-1]
blocks, err := c.bitcoinDg.GetBlocksByHeightRange(ctx, fromHeight, toHeight)
if err != nil {
logger.ErrorContext(ctx, "failed to get blocks",
slogx.Error(err),
slogx.Int64("from_height", fromHeight),
slogx.Int64("to_height", toHeight),
)
if err := subscription.SendError(ctx, errors.Wrapf(err, "failed to get blocks: from_height: %d, to_height: %d", fromHeight, toHeight)); err != nil {
logger.ErrorContext(ctx, "failed to send error", slogx.Error(err))
}
return nil
}
return blocks
})
}
}
}()
return subscription.Client(), nil
}
func (c *ClientDatabase) GetBlockHeader(ctx context.Context, height int64) (types.BlockHeader, error) {
header, err := c.bitcoinDg.GetBlockHeaderByHeight(ctx, height)
if err != nil {
return types.BlockHeader{}, errors.WithStack(err)
}
return header, nil
}
func (c *ClientDatabase) prepareRange(ctx context.Context, fromHeight, toHeight int64) (start, end int64, skip bool, err error) {
start = fromHeight
end = toHeight
// get current bitcoin block height
latestBlock, err := c.bitcoinDg.GetLatestBlockHeader(ctx)
if err != nil {
return -1, -1, false, errors.Wrap(err, "failed to get block count")
}
// set start to genesis block height
if start < 0 {
start = 0
}
// set end to current bitcoin block height if
// - end is -1
// - end is greater that current bitcoin block height
if end < 0 || end > latestBlock.Height {
end = latestBlock.Height
}
// if start is greater than end, skip this round
if start > end {
return -1, -1, true, nil
}
return start, end, false, nil
}
// GetTransactionByHash returns a transaction with the given hash. Returns errs.NotFound if transaction does not exist.
func (c *ClientDatabase) GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error) {
tx, err := c.bitcoinDg.GetTransactionByHash(ctx, txHash)
if err != nil {
return nil, errors.Wrap(err, "failed to get transaction by hash")
}
return tx, nil
}

View File

@@ -0,0 +1,12 @@
package btcclient
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/core/types"
)
type Contract interface {
GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error)
}

View File

@@ -0,0 +1,8 @@
package config
import "github.com/gaze-network/indexer-network/internal/postgres"
type Config struct {
Database string `mapstructure:"database"` // Database to store bitcoin data.
Postgres postgres.Config `mapstructure:"postgres"`
}

View File

@@ -0,0 +1,17 @@
package bitcoin
import (
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/core/types"
)
const (
Version = "v0.0.1"
DBVersion = 1
)
// DefaultCurrentBlockHeight is the default value for the current block height for first time indexing
var defaultCurrentBlock = types.BlockHeader{
Hash: common.ZeroHash,
Height: -1,
}

View File

@@ -0,0 +1,18 @@
BEGIN;
-- DROP INDEX
DROP INDEX IF EXISTS bitcoin_blocks_block_hash_idx;
DROP INDEX IF EXISTS bitcoin_transactions_block_hash_idx;
DROP INDEX IF EXISTS bitcoin_transactions_block_height_idx;
DROP INDEX IF EXISTS bitcoin_transaction_txouts_pkscript_idx;
DROP INDEX IF EXISTS bitcoin_transaction_txins_prevout_idx;
-- DROP TABLE
DROP TABLE IF EXISTS "bitcoin_indexer_stats";
DROP TABLE IF EXISTS "bitcoin_indexer_db_version";
DROP TABLE IF EXISTS "bitcoin_transaction_txins";
DROP TABLE IF EXISTS "bitcoin_transaction_txouts";
DROP TABLE IF EXISTS "bitcoin_transactions";
DROP TABLE IF EXISTS "bitcoin_blocks";
COMMIT;

View File

@@ -0,0 +1,71 @@
BEGIN;
-- Indexer Client Information
CREATE TABLE IF NOT EXISTS "bitcoin_indexer_stats" (
"id" BIGSERIAL PRIMARY KEY,
"client_version" TEXT NOT NULL,
"network" TEXT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "bitcoin_indexer_db_version" (
"id" BIGSERIAL PRIMARY KEY,
"version" INT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "bitcoin_indexer_db_version" ("version") VALUES (1);
-- Bitcoin Data
CREATE TABLE IF NOT EXISTS "bitcoin_blocks" (
"block_height" INT NOT NULL PRIMARY KEY,
"block_hash" TEXT NOT NULL,
"version" INT NOT NULL,
"merkle_root" TEXT NOT NULL,
"prev_block_hash" TEXT NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
"bits" BIGINT NOT NULL,
"nonce" BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS bitcoin_blocks_block_hash_idx ON "bitcoin_blocks" USING HASH ("block_hash");
CREATE TABLE IF NOT EXISTS "bitcoin_transactions" (
"tx_hash" TEXT NOT NULL PRIMARY KEY,
"version" INT NOT NULL,
"locktime" BIGINT NOT NULL,
"block_height" INT NOT NULL,
"block_hash" TEXT NOT NULL,
"idx" INT NOT NULL
);
CREATE INDEX IF NOT EXISTS bitcoin_transactions_block_height_idx ON "bitcoin_transactions" USING BTREE ("block_height");
CREATE INDEX IF NOT EXISTS bitcoin_transactions_block_hash_idx ON "bitcoin_transactions" USING BTREE ("block_hash");
CREATE TABLE IF NOT EXISTS "bitcoin_transaction_txouts" (
"tx_hash" TEXT NOT NULL,
"tx_idx" INT NOT NULL,
"pkscript" TEXT NOT NULL, -- Hex String
"value" BIGINT NOT NULL,
"is_spent" BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY ("tx_hash", "tx_idx")
);
CREATE INDEX IF NOT EXISTS bitcoin_transaction_txouts_pkscript_idx ON "bitcoin_transaction_txouts" USING HASH ("pkscript");
CREATE TABLE IF NOT EXISTS "bitcoin_transaction_txins" (
"tx_hash" TEXT NOT NULL,
"tx_idx" INT NOT NULL,
"prevout_tx_hash" TEXT NOT NULL,
"prevout_tx_idx" INT NOT NULL,
"prevout_pkscript" TEXT NULL, -- Hex String, Can be NULL if the prevout is a coinbase transaction
"scriptsig" TEXT NOT NULL, -- Hex String
"witness" TEXT, -- Hex String
"sequence" BIGINT NOT NULL,
PRIMARY KEY ("tx_hash", "tx_idx")
);
CREATE INDEX IF NOT EXISTS bitcoin_transaction_txins_prevout_idx ON "bitcoin_transaction_txins" USING BTREE ("prevout_tx_hash", "prevout_tx_idx");
COMMIT;

View File

@@ -0,0 +1,59 @@
-- name: GetLatestBlockHeader :one
SELECT * FROM bitcoin_blocks ORDER BY block_height DESC LIMIT 1;
-- name: InsertBlock :exec
INSERT INTO bitcoin_blocks ("block_height","block_hash","version","merkle_root","prev_block_hash","timestamp","bits","nonce") VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
-- name: InsertTransaction :exec
INSERT INTO bitcoin_transactions ("tx_hash","version","locktime","block_height","block_hash","idx") VALUES ($1, $2, $3, $4, $5, $6);
-- name: InsertTransactionTxOut :exec
INSERT INTO bitcoin_transaction_txouts ("tx_hash","tx_idx","pkscript","value") VALUES ($1, $2, $3, $4);
-- name: InsertTransactionTxIn :exec
WITH update_txout AS (
UPDATE "bitcoin_transaction_txouts"
SET "is_spent" = true
WHERE "tx_hash" = $3 AND "tx_idx" = $4 AND "is_spent" = false -- TODO: should throw an error if already spent
RETURNING "pkscript"
)
INSERT INTO bitcoin_transaction_txins ("tx_hash","tx_idx","prevout_tx_hash","prevout_tx_idx","prevout_pkscript","scriptsig","witness","sequence")
VALUES ($1, $2, $3, $4, (SELECT "pkscript" FROM update_txout), $5, $6, $7);
-- name: RevertData :exec
WITH delete_tx AS (
DELETE FROM "bitcoin_transactions" WHERE "block_height" >= @from_height
RETURNING "tx_hash"
), delete_txin AS (
DELETE FROM "bitcoin_transaction_txins" WHERE "tx_hash" = ANY(SELECT "tx_hash" FROM delete_tx)
RETURNING "prevout_tx_hash", "prevout_tx_idx"
), delete_txout AS (
DELETE FROM "bitcoin_transaction_txouts" WHERE "tx_hash" = ANY(SELECT "tx_hash" FROM delete_tx)
RETURNING "tx_hash", "tx_idx"
), revert_txout_spent AS (
UPDATE "bitcoin_transaction_txouts"
SET "is_spent" = false
WHERE
("tx_hash", "tx_idx") IN (SELECT "prevout_tx_hash", "prevout_tx_idx" FROM delete_txin) AND
("tx_hash", "tx_idx") NOT IN (SELECT "tx_hash", "tx_idx" FROM delete_txout) -- avoid to modified same row twice (modified the same row twice in a single statement is not supported)
RETURNING NULL
)
DELETE FROM "bitcoin_blocks" WHERE "bitcoin_blocks"."block_height" >= @from_height;
-- name: GetBlockByHeight :one
SELECT * FROM bitcoin_blocks WHERE block_height = $1;
-- name: GetBlocksByHeightRange :many
SELECT * FROM bitcoin_blocks WHERE block_height >= @from_height AND block_height <= @to_height ORDER BY block_height ASC;
-- name: GetTransactionsByHeightRange :many
SELECT * FROM bitcoin_transactions WHERE block_height >= @from_height AND block_height <= @to_height;
-- name: GetTransactionByHash :one
SELECT * FROM bitcoin_transactions WHERE tx_hash = $1;
-- name: GetTransactionTxOutsByTxHashes :many
SELECT * FROM bitcoin_transaction_txouts WHERE tx_hash = ANY(@tx_hashes::TEXT[]);
-- name: GetTransactionTxInsByTxHashes :many
SELECT * FROM bitcoin_transaction_txins WHERE tx_hash = ANY(@tx_hashes::TEXT[]);

View File

@@ -0,0 +1,8 @@
-- name: GetCurrentDBVersion :one
SELECT "version" FROM bitcoin_indexer_db_version ORDER BY id DESC LIMIT 1;
-- name: GetCurrentIndexerStats :one
SELECT "client_version", "network" FROM bitcoin_indexer_stats ORDER BY id DESC LIMIT 1;
-- name: UpdateIndexerStats :exec
INSERT INTO bitcoin_indexer_stats (client_version, network) VALUES ($1, $2);

View File

@@ -0,0 +1,25 @@
package datagateway
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/gaze-network/indexer-network/core/types"
)
type BitcoinDataGateway interface {
BitcoinWriterDataDataGateway
BitcoinReaderDataDataGateway
}
type BitcoinWriterDataDataGateway interface {
InsertBlock(context.Context, *types.Block) error
RevertBlocks(context.Context, int64) error
}
type BitcoinReaderDataDataGateway interface {
GetLatestBlockHeader(context.Context) (types.BlockHeader, error)
GetBlockHeaderByHeight(ctx context.Context, blockHeight int64) (types.BlockHeader, error)
GetBlocksByHeightRange(ctx context.Context, from int64, to int64) ([]*types.Block, error)
GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error)
}

View File

@@ -0,0 +1,13 @@
package datagateway
import (
"context"
"github.com/gaze-network/indexer-network/common"
)
type IndexerInformationDataGateway interface {
GetCurrentDBVersion(ctx context.Context) (int32, error)
GetLatestIndexerStats(ctx context.Context) (version string, network common.Network, err error)
UpdateIndexerStats(ctx context.Context, clientVersion string, network common.Network) error
}

View File

@@ -0,0 +1,142 @@
package bitcoin
import (
"cmp"
"context"
"log/slog"
"slices"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexers"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/modules/bitcoin/datagateway"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
)
// Make sure to implement the BitcoinProcessor interface
var _ indexers.BitcoinProcessor = (*Processor)(nil)
type Processor struct {
config config.Config
bitcoinDg datagateway.BitcoinDataGateway
indexerInfoDg datagateway.IndexerInformationDataGateway
}
func NewProcessor(config config.Config, bitcoinDg datagateway.BitcoinDataGateway, indexerInfoDg datagateway.IndexerInformationDataGateway) *Processor {
return &Processor{
config: config,
bitcoinDg: bitcoinDg,
indexerInfoDg: indexerInfoDg,
}
}
func (p Processor) Name() string {
return "Bitcoin"
}
func (p *Processor) Process(ctx context.Context, inputs []*types.Block) error {
if len(inputs) == 0 {
return nil
}
// Sort ASC by block height
slices.SortFunc(inputs, func(t1, t2 *types.Block) int {
return cmp.Compare(t1.Header.Height, t2.Header.Height)
})
latestBlock, err := p.CurrentBlock(ctx)
if err != nil {
return errors.Wrap(err, "failed to get latest indexed block header")
}
// check if the given blocks are continue from the latest indexed block
// return an error to prevent inserting out-of-order blocks or duplicate blocks
if inputs[0].Header.Height != latestBlock.Height+1 {
return errors.New("given blocks are not continue from the latest indexed block")
}
// check if the given blocks are in sequence and not missing any block
for i := 1; i < len(inputs); i++ {
if inputs[i].Header.Height != inputs[i-1].Header.Height+1 {
return errors.New("given blocks are not in sequence")
}
}
// Insert blocks
for _, b := range inputs {
err := p.bitcoinDg.InsertBlock(ctx, b)
if err != nil {
return errors.Wrapf(err, "failed to insert block, height: %d, hash: %s", b.Header.Height, b.Header.Hash)
}
logger.InfoContext(ctx, "Block inserted", slog.Int64("height", b.Header.Height), slogx.Stringer("hash", b.Header.Hash))
}
return nil
}
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
b, err := p.bitcoinDg.GetLatestBlockHeader(ctx)
if err != nil {
if errors.Is(err, errs.NotFound) {
return defaultCurrentBlock, nil
}
return types.BlockHeader{}, errors.WithStack(err)
}
return b, nil
}
func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error) {
header, err := p.bitcoinDg.GetBlockHeaderByHeight(ctx, height)
if err != nil {
return types.BlockHeader{}, errors.WithStack(err)
}
return header, nil
}
func (p *Processor) RevertData(ctx context.Context, from int64) error {
if err := p.bitcoinDg.RevertBlocks(ctx, from); err != nil {
return errors.WithStack(err)
}
return nil
}
func (p *Processor) VerifyStates(ctx context.Context) error {
// Check current db version with the required db version
{
dbVersion, err := p.indexerInfoDg.GetCurrentDBVersion(ctx)
if err != nil {
return errors.Wrap(err, "can't get current db version")
}
if dbVersion != DBVersion {
return errors.Wrapf(errs.ConflictSetting, "db version mismatch, please upgrade to version %d", DBVersion)
}
}
// Check if the latest indexed network is mismatched with configured network
{
_, network, err := p.indexerInfoDg.GetLatestIndexerStats(ctx)
if err != nil {
if errors.Is(err, errs.NotFound) {
goto end
}
return errors.Wrap(err, "can't get latest indexer stats")
}
if network != p.config.Network {
return errors.Wrapf(errs.ConflictSetting, "network mismatch, latest indexed network: %q, configured network: %q. If you want to change the network, please reset the database", network, p.config.Network)
}
}
// TODO: Verify the states of the indexed data to ensure the last shutdown was graceful and no missing data.
end:
if err := p.indexerInfoDg.UpdateIndexerStats(ctx, Version, p.config.Network); err != nil {
return errors.Wrap(err, "can't update indexer stats")
}
return nil
}

View File

@@ -0,0 +1,171 @@
package postgres
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/bitcoin/repository/postgres/gen"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
)
func (r *Repository) GetLatestBlockHeader(ctx context.Context) (types.BlockHeader, error) {
model, err := r.queries.GetLatestBlockHeader(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return types.BlockHeader{}, errors.Join(errs.NotFound, err)
}
return types.BlockHeader{}, errors.Wrap(err, "failed to get latest block header")
}
data, err := mapBlockHeaderModelToType(model)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "failed to map block header model to type")
}
return data, nil
}
func (r *Repository) InsertBlock(ctx context.Context, block *types.Block) error {
blockParams, txParams, txoutParams, txinParams := mapBlockTypeToParams(block)
tx, err := r.db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback(ctx)
queries := r.queries.WithTx(tx)
if err := queries.InsertBlock(ctx, blockParams); err != nil {
return errors.Wrapf(err, "failed to insert block, height: %d, hash: %s", blockParams.BlockHeight, blockParams.BlockHash)
}
for _, params := range txParams {
if err := queries.InsertTransaction(ctx, params); err != nil {
return errors.Wrapf(err, "failed to insert transaction, hash: %s", params.TxHash)
}
}
// Should insert txout first, then txin
// Because txin references txout
for _, params := range txoutParams {
if err := queries.InsertTransactionTxOut(ctx, params); err != nil {
return errors.Wrapf(err, "failed to insert transaction txout, %v:%v", params.TxHash, params.TxIdx)
}
}
for _, params := range txinParams {
if err := queries.InsertTransactionTxIn(ctx, params); err != nil {
return errors.Wrapf(err, "failed to insert transaction txin, %v:%v", params.TxHash, params.TxIdx)
}
}
if err := tx.Commit(ctx); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
func (r *Repository) RevertBlocks(ctx context.Context, from int64) error {
tx, err := r.db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback(ctx)
queries := r.queries.WithTx(tx)
if err := queries.RevertData(ctx, int32(from)); err != nil && !errors.Is(err, pgx.ErrNoRows) {
return errors.Wrap(err, "failed to revert data")
}
if err := tx.Commit(ctx); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
func (r *Repository) GetBlockHeaderByHeight(ctx context.Context, blockHeight int64) (types.BlockHeader, error) {
blockModel, err := r.queries.GetBlockByHeight(ctx, int32(blockHeight))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return types.BlockHeader{}, errors.Join(errs.NotFound, err)
}
return types.BlockHeader{}, errors.Wrap(err, "failed to get block by height")
}
data, err := mapBlockHeaderModelToType(blockModel)
if err != nil {
return types.BlockHeader{}, errors.Wrap(err, "failed to map block header model to type")
}
return data, nil
}
func (r *Repository) GetBlocksByHeightRange(ctx context.Context, from int64, to int64) ([]*types.Block, error) {
blocks, err := r.queries.GetBlocksByHeightRange(ctx, gen.GetBlocksByHeightRangeParams{
FromHeight: int32(from),
ToHeight: int32(to),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get blocks by height range")
}
if len(blocks) == 0 {
return []*types.Block{}, nil
}
txs, err := r.queries.GetTransactionsByHeightRange(ctx, gen.GetTransactionsByHeightRangeParams{
FromHeight: int32(from),
ToHeight: int32(to),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get transactions by height range")
}
txHashes := lo.Map(txs, func(tx gen.BitcoinTransaction, _ int) string { return tx.TxHash })
txOuts, err := r.queries.GetTransactionTxOutsByTxHashes(ctx, txHashes)
if err != nil {
return nil, errors.Wrap(err, "failed to get transaction txouts by tx hashes")
}
txIns, err := r.queries.GetTransactionTxInsByTxHashes(ctx, txHashes)
if err != nil {
return nil, errors.Wrap(err, "failed to get transaction txins by tx hashes")
}
// Grouping result by block height and tx hash
groupedTxs := lo.GroupBy(txs, func(tx gen.BitcoinTransaction) int32 { return tx.BlockHeight })
groupedTxOuts := lo.GroupBy(txOuts, func(txOut gen.BitcoinTransactionTxout) string { return txOut.TxHash })
groupedTxIns := lo.GroupBy(txIns, func(txIn gen.BitcoinTransactionTxin) string { return txIn.TxHash })
var errs []error
result := lo.Map(blocks, func(blockModel gen.BitcoinBlock, _ int) *types.Block {
header, err := mapBlockHeaderModelToType(blockModel)
if err != nil {
errs = append(errs, errors.Wrap(err, "failed to map block header model to type"))
return nil
}
txsModel := groupedTxs[blockModel.BlockHeight]
return &types.Block{
Header: header,
Transactions: lo.Map(txsModel, func(txModel gen.BitcoinTransaction, _ int) *types.Transaction {
tx, err := mapTransactionModelToType(txModel, groupedTxIns[txModel.TxHash], groupedTxOuts[txModel.TxHash])
if err != nil {
errs = append(errs, errors.Wrap(err, "failed to map transaction model to type"))
return nil
}
return &tx
}),
}
})
if len(errs) > 0 {
return nil, errors.Wrap(errors.Join(errs...), "failed while mapping result")
}
return result, nil
}

View File

@@ -0,0 +1,342 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: data.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const getBlockByHeight = `-- name: GetBlockByHeight :one
SELECT block_height, block_hash, version, merkle_root, prev_block_hash, timestamp, bits, nonce FROM bitcoin_blocks WHERE block_height = $1
`
func (q *Queries) GetBlockByHeight(ctx context.Context, blockHeight int32) (BitcoinBlock, error) {
row := q.db.QueryRow(ctx, getBlockByHeight, blockHeight)
var i BitcoinBlock
err := row.Scan(
&i.BlockHeight,
&i.BlockHash,
&i.Version,
&i.MerkleRoot,
&i.PrevBlockHash,
&i.Timestamp,
&i.Bits,
&i.Nonce,
)
return i, err
}
const getBlocksByHeightRange = `-- name: GetBlocksByHeightRange :many
SELECT block_height, block_hash, version, merkle_root, prev_block_hash, timestamp, bits, nonce FROM bitcoin_blocks WHERE block_height >= $1 AND block_height <= $2 ORDER BY block_height ASC
`
type GetBlocksByHeightRangeParams struct {
FromHeight int32
ToHeight int32
}
func (q *Queries) GetBlocksByHeightRange(ctx context.Context, arg GetBlocksByHeightRangeParams) ([]BitcoinBlock, error) {
rows, err := q.db.Query(ctx, getBlocksByHeightRange, arg.FromHeight, arg.ToHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BitcoinBlock
for rows.Next() {
var i BitcoinBlock
if err := rows.Scan(
&i.BlockHeight,
&i.BlockHash,
&i.Version,
&i.MerkleRoot,
&i.PrevBlockHash,
&i.Timestamp,
&i.Bits,
&i.Nonce,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestBlockHeader = `-- name: GetLatestBlockHeader :one
SELECT block_height, block_hash, version, merkle_root, prev_block_hash, timestamp, bits, nonce FROM bitcoin_blocks ORDER BY block_height DESC LIMIT 1
`
func (q *Queries) GetLatestBlockHeader(ctx context.Context) (BitcoinBlock, error) {
row := q.db.QueryRow(ctx, getLatestBlockHeader)
var i BitcoinBlock
err := row.Scan(
&i.BlockHeight,
&i.BlockHash,
&i.Version,
&i.MerkleRoot,
&i.PrevBlockHash,
&i.Timestamp,
&i.Bits,
&i.Nonce,
)
return i, err
}
const getTransactionByHash = `-- name: GetTransactionByHash :one
SELECT tx_hash, version, locktime, block_height, block_hash, idx FROM bitcoin_transactions WHERE tx_hash = $1
`
func (q *Queries) GetTransactionByHash(ctx context.Context, txHash string) (BitcoinTransaction, error) {
row := q.db.QueryRow(ctx, getTransactionByHash, txHash)
var i BitcoinTransaction
err := row.Scan(
&i.TxHash,
&i.Version,
&i.Locktime,
&i.BlockHeight,
&i.BlockHash,
&i.Idx,
)
return i, err
}
const getTransactionTxInsByTxHashes = `-- name: GetTransactionTxInsByTxHashes :many
SELECT tx_hash, tx_idx, prevout_tx_hash, prevout_tx_idx, prevout_pkscript, scriptsig, witness, sequence FROM bitcoin_transaction_txins WHERE tx_hash = ANY($1::TEXT[])
`
func (q *Queries) GetTransactionTxInsByTxHashes(ctx context.Context, txHashes []string) ([]BitcoinTransactionTxin, error) {
rows, err := q.db.Query(ctx, getTransactionTxInsByTxHashes, txHashes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BitcoinTransactionTxin
for rows.Next() {
var i BitcoinTransactionTxin
if err := rows.Scan(
&i.TxHash,
&i.TxIdx,
&i.PrevoutTxHash,
&i.PrevoutTxIdx,
&i.PrevoutPkscript,
&i.Scriptsig,
&i.Witness,
&i.Sequence,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTransactionTxOutsByTxHashes = `-- name: GetTransactionTxOutsByTxHashes :many
SELECT tx_hash, tx_idx, pkscript, value, is_spent FROM bitcoin_transaction_txouts WHERE tx_hash = ANY($1::TEXT[])
`
func (q *Queries) GetTransactionTxOutsByTxHashes(ctx context.Context, txHashes []string) ([]BitcoinTransactionTxout, error) {
rows, err := q.db.Query(ctx, getTransactionTxOutsByTxHashes, txHashes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BitcoinTransactionTxout
for rows.Next() {
var i BitcoinTransactionTxout
if err := rows.Scan(
&i.TxHash,
&i.TxIdx,
&i.Pkscript,
&i.Value,
&i.IsSpent,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTransactionsByHeightRange = `-- name: GetTransactionsByHeightRange :many
SELECT tx_hash, version, locktime, block_height, block_hash, idx FROM bitcoin_transactions WHERE block_height >= $1 AND block_height <= $2
`
type GetTransactionsByHeightRangeParams struct {
FromHeight int32
ToHeight int32
}
func (q *Queries) GetTransactionsByHeightRange(ctx context.Context, arg GetTransactionsByHeightRangeParams) ([]BitcoinTransaction, error) {
rows, err := q.db.Query(ctx, getTransactionsByHeightRange, arg.FromHeight, arg.ToHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BitcoinTransaction
for rows.Next() {
var i BitcoinTransaction
if err := rows.Scan(
&i.TxHash,
&i.Version,
&i.Locktime,
&i.BlockHeight,
&i.BlockHash,
&i.Idx,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertBlock = `-- name: InsertBlock :exec
INSERT INTO bitcoin_blocks ("block_height","block_hash","version","merkle_root","prev_block_hash","timestamp","bits","nonce") VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
type InsertBlockParams struct {
BlockHeight int32
BlockHash string
Version int32
MerkleRoot string
PrevBlockHash string
Timestamp pgtype.Timestamptz
Bits int64
Nonce int64
}
func (q *Queries) InsertBlock(ctx context.Context, arg InsertBlockParams) error {
_, err := q.db.Exec(ctx, insertBlock,
arg.BlockHeight,
arg.BlockHash,
arg.Version,
arg.MerkleRoot,
arg.PrevBlockHash,
arg.Timestamp,
arg.Bits,
arg.Nonce,
)
return err
}
const insertTransaction = `-- name: InsertTransaction :exec
INSERT INTO bitcoin_transactions ("tx_hash","version","locktime","block_height","block_hash","idx") VALUES ($1, $2, $3, $4, $5, $6)
`
type InsertTransactionParams struct {
TxHash string
Version int32
Locktime int64
BlockHeight int32
BlockHash string
Idx int32
}
func (q *Queries) InsertTransaction(ctx context.Context, arg InsertTransactionParams) error {
_, err := q.db.Exec(ctx, insertTransaction,
arg.TxHash,
arg.Version,
arg.Locktime,
arg.BlockHeight,
arg.BlockHash,
arg.Idx,
)
return err
}
const insertTransactionTxIn = `-- name: InsertTransactionTxIn :exec
WITH update_txout AS (
UPDATE "bitcoin_transaction_txouts"
SET "is_spent" = true
WHERE "tx_hash" = $3 AND "tx_idx" = $4 AND "is_spent" = false -- TODO: should throw an error if already spent
RETURNING "pkscript"
)
INSERT INTO bitcoin_transaction_txins ("tx_hash","tx_idx","prevout_tx_hash","prevout_tx_idx","prevout_pkscript","scriptsig","witness","sequence")
VALUES ($1, $2, $3, $4, (SELECT "pkscript" FROM update_txout), $5, $6, $7)
`
type InsertTransactionTxInParams struct {
TxHash string
TxIdx int32
PrevoutTxHash string
PrevoutTxIdx int32
Scriptsig string
Witness pgtype.Text
Sequence int64
}
func (q *Queries) InsertTransactionTxIn(ctx context.Context, arg InsertTransactionTxInParams) error {
_, err := q.db.Exec(ctx, insertTransactionTxIn,
arg.TxHash,
arg.TxIdx,
arg.PrevoutTxHash,
arg.PrevoutTxIdx,
arg.Scriptsig,
arg.Witness,
arg.Sequence,
)
return err
}
const insertTransactionTxOut = `-- name: InsertTransactionTxOut :exec
INSERT INTO bitcoin_transaction_txouts ("tx_hash","tx_idx","pkscript","value") VALUES ($1, $2, $3, $4)
`
type InsertTransactionTxOutParams struct {
TxHash string
TxIdx int32
Pkscript string
Value int64
}
func (q *Queries) InsertTransactionTxOut(ctx context.Context, arg InsertTransactionTxOutParams) error {
_, err := q.db.Exec(ctx, insertTransactionTxOut,
arg.TxHash,
arg.TxIdx,
arg.Pkscript,
arg.Value,
)
return err
}
const revertData = `-- name: RevertData :exec
WITH delete_tx AS (
DELETE FROM "bitcoin_transactions" WHERE "block_height" >= $1
RETURNING "tx_hash"
), delete_txin AS (
DELETE FROM "bitcoin_transaction_txins" WHERE "tx_hash" = ANY(SELECT "tx_hash" FROM delete_tx)
RETURNING "prevout_tx_hash", "prevout_tx_idx"
), delete_txout AS (
DELETE FROM "bitcoin_transaction_txouts" WHERE "tx_hash" = ANY(SELECT "tx_hash" FROM delete_tx)
RETURNING "tx_hash", "tx_idx"
), revert_txout_spent AS (
UPDATE "bitcoin_transaction_txouts"
SET "is_spent" = false
WHERE
("tx_hash", "tx_idx") IN (SELECT "prevout_tx_hash", "prevout_tx_idx" FROM delete_txin) AND
("tx_hash", "tx_idx") NOT IN (SELECT "tx_hash", "tx_idx" FROM delete_txout) -- avoid to modified same row twice (modified the same row twice in a single statement is not supported)
RETURNING NULL
)
DELETE FROM "bitcoin_blocks" WHERE "bitcoin_blocks"."block_height" >= $1
`
func (q *Queries) RevertData(ctx context.Context, fromHeight int32) error {
_, err := q.db.Exec(ctx, revertData, fromHeight)
return err
}

View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package gen
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,51 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: info.sql
package gen
import (
"context"
)
const getCurrentDBVersion = `-- name: GetCurrentDBVersion :one
SELECT "version" FROM bitcoin_indexer_db_version ORDER BY id DESC LIMIT 1
`
func (q *Queries) GetCurrentDBVersion(ctx context.Context) (int32, error) {
row := q.db.QueryRow(ctx, getCurrentDBVersion)
var version int32
err := row.Scan(&version)
return version, err
}
const getCurrentIndexerStats = `-- name: GetCurrentIndexerStats :one
SELECT "client_version", "network" FROM bitcoin_indexer_stats ORDER BY id DESC LIMIT 1
`
type GetCurrentIndexerStatsRow struct {
ClientVersion string
Network string
}
func (q *Queries) GetCurrentIndexerStats(ctx context.Context) (GetCurrentIndexerStatsRow, error) {
row := q.db.QueryRow(ctx, getCurrentIndexerStats)
var i GetCurrentIndexerStatsRow
err := row.Scan(&i.ClientVersion, &i.Network)
return i, err
}
const updateIndexerStats = `-- name: UpdateIndexerStats :exec
INSERT INTO bitcoin_indexer_stats (client_version, network) VALUES ($1, $2)
`
type UpdateIndexerStatsParams struct {
ClientVersion string
Network string
}
func (q *Queries) UpdateIndexerStats(ctx context.Context, arg UpdateIndexerStatsParams) error {
_, err := q.db.Exec(ctx, updateIndexerStats, arg.ClientVersion, arg.Network)
return err
}

View File

@@ -0,0 +1,61 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package gen
import (
"github.com/jackc/pgx/v5/pgtype"
)
type BitcoinBlock struct {
BlockHeight int32
BlockHash string
Version int32
MerkleRoot string
PrevBlockHash string
Timestamp pgtype.Timestamptz
Bits int64
Nonce int64
}
type BitcoinIndexerDbVersion struct {
Id int64
Version int32
CreatedAt pgtype.Timestamptz
}
type BitcoinIndexerStat struct {
Id int64
ClientVersion string
Network string
CreatedAt pgtype.Timestamptz
}
type BitcoinTransaction struct {
TxHash string
Version int32
Locktime int64
BlockHeight int32
BlockHash string
Idx int32
}
type BitcoinTransactionTxin struct {
TxHash string
TxIdx int32
PrevoutTxHash string
PrevoutTxIdx int32
PrevoutPkscript pgtype.Text
Scriptsig string
Witness pgtype.Text
Sequence int64
}
type BitcoinTransactionTxout struct {
TxHash string
TxIdx int32
Pkscript string
Value int64
IsSpent bool
}

View File

@@ -0,0 +1,44 @@
package postgres
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/bitcoin/datagateway"
"github.com/gaze-network/indexer-network/modules/bitcoin/repository/postgres/gen"
"github.com/jackc/pgx/v5"
)
// Make sure Repository implements the IndexerInformationDataGateway interface
var _ datagateway.IndexerInformationDataGateway = (*Repository)(nil)
func (r *Repository) GetCurrentDBVersion(ctx context.Context) (int32, error) {
version, err := r.queries.GetCurrentDBVersion(ctx)
if err != nil {
return 0, errors.WithStack(err)
}
return version, nil
}
func (r *Repository) GetLatestIndexerStats(ctx context.Context) (string, common.Network, error) {
stats, err := r.queries.GetCurrentIndexerStats(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return "", "", errors.Join(errs.NotFound, err)
}
return "", "", errors.WithStack(err)
}
return stats.ClientVersion, common.Network(stats.Network), nil
}
func (r *Repository) UpdateIndexerStats(ctx context.Context, clientVersion string, network common.Network) error {
if err := r.queries.UpdateIndexerStats(ctx, gen.UpdateIndexerStatsParams{
ClientVersion: clientVersion,
Network: network.String(),
}); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@@ -0,0 +1,187 @@
package postgres
import (
"cmp"
"encoding/hex"
"slices"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/bitcoin/repository/postgres/gen"
"github.com/gaze-network/indexer-network/pkg/btcutils"
"github.com/jackc/pgx/v5/pgtype"
)
func mapBlockHeaderTypeToModel(src types.BlockHeader) gen.BitcoinBlock {
return gen.BitcoinBlock{
BlockHeight: int32(src.Height),
BlockHash: src.Hash.String(),
Version: src.Version,
MerkleRoot: src.MerkleRoot.String(),
PrevBlockHash: src.PrevBlock.String(),
Timestamp: pgtype.Timestamptz{
Time: src.Timestamp,
Valid: true,
},
Bits: int64(src.Bits),
Nonce: int64(src.Nonce),
}
}
func mapBlockHeaderModelToType(src gen.BitcoinBlock) (types.BlockHeader, error) {
hash, err := chainhash.NewHashFromStr(src.BlockHash)
if err != nil {
return types.BlockHeader{}, errors.Join(errors.Wrap(err, "failed to parse block hash"), errs.InternalError)
}
prevHash, err := chainhash.NewHashFromStr(src.PrevBlockHash)
if err != nil {
return types.BlockHeader{}, errors.Join(errors.Wrap(err, "failed to parse prev block hash"), errs.InternalError)
}
merkleRoot, err := chainhash.NewHashFromStr(src.MerkleRoot)
if err != nil {
return types.BlockHeader{}, errors.Join(errors.Wrap(err, "failed to parse merkle root"), errs.InternalError)
}
return types.BlockHeader{
Hash: *hash,
Height: int64(src.BlockHeight),
Version: src.Version,
PrevBlock: *prevHash,
MerkleRoot: *merkleRoot,
Timestamp: src.Timestamp.Time,
Bits: uint32(src.Bits),
Nonce: uint32(src.Nonce),
}, nil
}
func mapBlockTypeToParams(src *types.Block) (gen.InsertBlockParams, []gen.InsertTransactionParams, []gen.InsertTransactionTxOutParams, []gen.InsertTransactionTxInParams) {
txs := make([]gen.InsertTransactionParams, 0, len(src.Transactions))
txouts := make([]gen.InsertTransactionTxOutParams, 0)
txins := make([]gen.InsertTransactionTxInParams, 0)
block := gen.InsertBlockParams{
BlockHeight: int32(src.Header.Height),
BlockHash: src.Header.Hash.String(),
Version: src.Header.Version,
MerkleRoot: src.Header.MerkleRoot.String(),
PrevBlockHash: src.Header.PrevBlock.String(),
Timestamp: pgtype.Timestamptz{
Time: src.Header.Timestamp,
Valid: true,
},
Bits: int64(src.Header.Bits),
Nonce: int64(src.Header.Nonce),
}
for txIdx, srcTx := range src.Transactions {
tx := gen.InsertTransactionParams{
TxHash: srcTx.TxHash.String(),
Version: srcTx.Version,
Locktime: int64(srcTx.LockTime),
BlockHeight: int32(src.Header.Height),
BlockHash: src.Header.Hash.String(),
Idx: int32(txIdx),
}
txs = append(txs, tx)
for idx, txin := range srcTx.TxIn {
var witness pgtype.Text
if len(txin.Witness) > 0 {
witness = pgtype.Text{
String: btcutils.WitnessToString(txin.Witness),
Valid: true,
}
}
txins = append(txins, gen.InsertTransactionTxInParams{
TxHash: tx.TxHash,
TxIdx: int32(idx),
PrevoutTxHash: txin.PreviousOutTxHash.String(),
PrevoutTxIdx: int32(txin.PreviousOutIndex),
Scriptsig: hex.EncodeToString(txin.SignatureScript),
Witness: witness,
Sequence: int64(txin.Sequence),
})
}
for idx, txout := range srcTx.TxOut {
txouts = append(txouts, gen.InsertTransactionTxOutParams{
TxHash: tx.TxHash,
TxIdx: int32(idx),
Pkscript: hex.EncodeToString(txout.PkScript),
Value: txout.Value,
})
}
}
return block, txs, txouts, txins
}
func mapTransactionModelToType(src gen.BitcoinTransaction, txInModel []gen.BitcoinTransactionTxin, txOutModels []gen.BitcoinTransactionTxout) (types.Transaction, error) {
blockHash, err := chainhash.NewHashFromStr(src.BlockHash)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to parse block hash")
}
txHash, err := chainhash.NewHashFromStr(src.TxHash)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to parse tx hash")
}
// Sort txins and txouts by index (Asc)
slices.SortFunc(txOutModels, func(i, j gen.BitcoinTransactionTxout) int {
return cmp.Compare(i.TxIdx, j.TxIdx)
})
slices.SortFunc(txInModel, func(i, j gen.BitcoinTransactionTxin) int {
return cmp.Compare(i.TxIdx, j.TxIdx)
})
txIns := make([]*types.TxIn, 0, len(txInModel))
txOuts := make([]*types.TxOut, 0, len(txOutModels))
for _, txInModel := range txInModel {
scriptsig, err := hex.DecodeString(txInModel.Scriptsig)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to decode scriptsig")
}
prevoutTxHash, err := chainhash.NewHashFromStr(txInModel.PrevoutTxHash)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to parse prevout tx hash")
}
var witness [][]byte
if txInModel.Witness.Valid {
w, err := btcutils.WitnessFromString(txInModel.Witness.String)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to parse witness from hex string")
}
witness = w
}
txIns = append(txIns, &types.TxIn{
SignatureScript: scriptsig,
Witness: witness,
Sequence: uint32(txInModel.Sequence),
PreviousOutIndex: uint32(txInModel.PrevoutTxIdx),
PreviousOutTxHash: *prevoutTxHash,
})
}
for _, txOutModel := range txOutModels {
pkscript, err := hex.DecodeString(txOutModel.Pkscript)
if err != nil {
return types.Transaction{}, errors.Wrap(err, "failed to decode pkscript")
}
txOuts = append(txOuts, &types.TxOut{
PkScript: pkscript,
Value: txOutModel.Value,
})
}
return types.Transaction{
BlockHeight: int64(src.BlockHeight),
BlockHash: *blockHash,
Index: uint32(src.Idx),
TxHash: *txHash,
Version: src.Version,
LockTime: uint32(src.Locktime),
TxIn: txIns,
TxOut: txOuts,
}, nil
}

View File

@@ -0,0 +1,22 @@
package postgres
import (
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/bitcoin/datagateway"
"github.com/gaze-network/indexer-network/modules/bitcoin/repository/postgres/gen"
)
// Make sure Repository implements the BitcoinDataGateway interface
var _ datagateway.BitcoinDataGateway = (*Repository)(nil)
type Repository struct {
db postgres.DB
queries *gen.Queries
}
func NewRepository(db postgres.DB) *Repository {
return &Repository{
db: db,
queries: gen.New(db),
}
}

View File

@@ -0,0 +1,35 @@
package postgres
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/types"
"github.com/jackc/pgx/v5"
)
func (r *Repository) GetTransactionByHash(ctx context.Context, txHash chainhash.Hash) (*types.Transaction, error) {
model, err := r.queries.GetTransactionByHash(ctx, txHash.String())
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.Join(errs.NotFound, err)
}
return nil, errors.Wrap(err, "failed to get transaction by hash")
}
txIns, err := r.queries.GetTransactionTxInsByTxHashes(ctx, []string{txHash.String()})
if err != nil {
return nil, errors.Wrap(err, "failed to get transaction txins by tx hashes")
}
txOuts, err := r.queries.GetTransactionTxOutsByTxHashes(ctx, []string{txHash.String()})
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, errors.Wrap(err, "failed to get transaction txouts by tx hashes")
}
tx, err := mapTransactionModelToType(model, txIns, txOuts)
if err != nil {
return nil, errors.Wrap(err, "failed to map transaction model to type")
}
return &tx, nil
}

1
modules/bitcoin/types.go Normal file
View File

@@ -0,0 +1 @@
package bitcoin

View File

@@ -124,7 +124,6 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
}
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
terms := lo.FromPtr(runeEntry.Terms)
resp := getTokenInfoResponse{
Result: &getTokenInfoResult{
Id: runeId,
@@ -148,12 +147,12 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
Spacers: runeEntry.SpacedRune.Spacers,
Symbol: string(runeEntry.Symbol),
Terms: entryTerms{
Amount: lo.FromPtr(terms.Amount),
Cap: lo.FromPtr(terms.Cap),
HeightStart: terms.HeightStart,
HeightEnd: terms.HeightEnd,
OffsetStart: terms.OffsetStart,
OffsetEnd: terms.OffsetEnd,
Amount: lo.FromPtr(runeEntry.Terms.Amount),
Cap: lo.FromPtr(runeEntry.Terms.Cap),
HeightStart: runeEntry.Terms.HeightStart,
HeightEnd: runeEntry.Terms.HeightEnd,
OffsetStart: runeEntry.Terms.OffsetStart,
OffsetEnd: runeEntry.Terms.OffsetEnd,
},
Turbo: runeEntry.Turbo,
},

View File

@@ -1,13 +1,13 @@
package httphandler
import (
"bytes"
"encoding/hex"
"fmt"
"slices"
"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/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/uint128"
"github.com/gofiber/fiber/v2"
@@ -15,11 +15,9 @@ import (
)
type getTransactionsRequest struct {
Wallet string `query:"wallet"`
Id string `query:"id"`
FromBlock int64 `query:"fromBlock"`
ToBlock int64 `query:"toBlock"`
Wallet string `query:"wallet"`
Id string `query:"id"`
BlockHeight uint64 `query:"blockHeight"`
}
func (r getTransactionsRequest) Validate() error {
@@ -27,12 +25,6 @@ func (r getTransactionsRequest) Validate() 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"))
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
@@ -92,7 +84,6 @@ type amountWithDecimal struct {
type transaction struct {
TxHash chainhash.Hash `json:"txHash"`
BlockHeight uint64 `json:"blockHeight"`
Index uint32 `json:"index"`
Timestamp int64 `json:"timestamp"`
Inputs []txInputOutput `json:"inputs"`
Outputs []txInputOutput `json:"outputs"`
@@ -125,6 +116,15 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
}
}
blockHeight := req.BlockHeight
if blockHeight == 0 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
blockHeight = uint64(blockHeader.Height)
}
var runeId runes.RuneId
if req.Id != "" {
var ok bool
@@ -134,37 +134,68 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
}
}
// 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 {
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
if err != nil {
return errors.Wrap(err, "error during GetLatestBlock")
}
if req.FromBlock == -1 {
req.FromBlock = blockHeader.Height
}
if req.ToBlock == -1 {
req.ToBlock = 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))
txs, err := h.usecase.GetTransactionsByHeight(ctx.UserContext(), blockHeight)
if err != nil {
return errors.Wrap(err, "error during GetRuneTransactions")
return errors.Wrap(err, "error during GetTransactionsByHeight")
}
var allRuneIds []runes.RuneId
filteredTxs := make([]*entity.RuneTransaction, 0)
isTxContainPkScript := func(tx *entity.RuneTransaction) bool {
for _, input := range tx.Inputs {
if bytes.Equal(input.PkScript, pkScript) {
return true
}
}
for _, output := range tx.Outputs {
if bytes.Equal(output.PkScript, pkScript) {
return true
}
}
return false
}
isTxContainRuneId := func(tx *entity.RuneTransaction) bool {
for _, input := range tx.Inputs {
if input.RuneId == runeId {
return true
}
}
for _, output := range tx.Outputs {
if output.RuneId == runeId {
return true
}
}
for mintedRuneId := range tx.Mints {
if mintedRuneId == runeId {
return true
}
}
for burnedRuneId := range tx.Burns {
if burnedRuneId == runeId {
return true
}
}
if tx.Runestone != nil {
if tx.Runestone.Mint != nil && *tx.Runestone.Mint == runeId {
return true
}
// returns true if this tx etched this runeId
if tx.RuneEtched && tx.BlockHeight == runeId.BlockHeight && tx.Index == runeId.TxIndex {
return true
}
}
return false
}
for _, tx := range txs {
if pkScript != nil && !isTxContainPkScript(tx) {
continue
}
if runeId != (runes.RuneId{}) && !isTxContainRuneId(tx) {
continue
}
filteredTxs = append(filteredTxs, tx)
}
var allRuneIds []runes.RuneId
for _, tx := range filteredTxs {
for id := range tx.Mints {
allRuneIds = append(allRuneIds, id)
}
@@ -184,12 +215,11 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
txList := make([]transaction, 0, len(txs))
for _, tx := range txs {
txList := make([]transaction, 0, len(filteredTxs))
for _, tx := range filteredTxs {
respTx := transaction{
TxHash: tx.Hash,
BlockHeight: tx.BlockHeight,
Index: tx.Index,
Timestamp: tx.Timestamp.Unix(),
Inputs: make([]txInputOutput, 0, len(tx.Inputs)),
Outputs: make([]txInputOutput, 0, len(tx.Outputs)),
@@ -279,13 +309,6 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
}
txList = append(txList, respTx)
}
// sort by block height ASC, then index ASC
slices.SortFunc(txList, func(t1, t2 transaction) int {
if t1.BlockHeight != t2.BlockHeight {
return int(t1.BlockHeight - t2.BlockHeight)
}
return int(t1.Index - t2.Index)
})
resp := getTransactionsResponse{
Result: &getTransactionsResult{

View File

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

View File

@@ -72,7 +72,6 @@ CREATE TABLE IF NOT EXISTS "runes_transactions" (
"rune_etched" BOOLEAN NOT NULL
);
CREATE INDEX IF NOT EXISTS runes_transactions_block_height_idx ON "runes_transactions" USING BTREE ("block_height");
CREATE INDEX IF NOT EXISTS runes_transactions_jsonb_idx ON "runes_transactions" USING GIN ("inputs", "outputs", "mints", "burns");
CREATE TABLE IF NOT EXISTS "runes_runestones" (
"tx_hash" TEXT NOT NULL PRIMARY KEY,

View File

@@ -40,24 +40,10 @@ SELECT * FROM runes_entries
-- name: GetRuneIdFromRune :one
SELECT rune_id FROM runes_entries WHERE rune = $1;
-- name: GetRuneTransactions :many
SELECT * FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE (
@filter_pk_script::BOOLEAN = FALSE -- if @filter_pk_script is TRUE, apply pk_script filter
OR runes_transactions.outputs @> @pk_script_param::JSONB
OR runes_transactions.inputs @> @pk_script_param::JSONB
) AND (
@filter_rune_id::BOOLEAN = FALSE -- if @filter_rune_id is TRUE, apply rune_id filter
OR runes_transactions.outputs @> @rune_id_param::JSONB
OR runes_transactions.inputs @> @rune_id_param::JSONB
OR runes_transactions.mints ? @rune_id
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 LIMIT 10000;
-- name: GetRuneTransactionsByHeight :many
SELECT * FROM runes_transactions
LEFT JOIN runes_runestones ON runes_transactions.hash = runes_runestones.tx_hash
WHERE runes_transactions.block_height = $1;
-- name: CountRuneEntries :one
SELECT COUNT(*) FROM runes_entries;

View File

@@ -26,8 +26,7 @@ type RunesDataGatewayWithTx interface {
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) ([]*entity.RuneTransaction, error)
GetRuneTransactionsByHeight(ctx context.Context, height uint64) ([]*entity.RuneTransaction, error)
GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]*entity.OutPointBalance, error)
GetUnspentOutPointBalancesByPkScript(ctx context.Context, pkScript []byte, blockHeight uint64) ([]*entity.OutPointBalance, error)

View File

@@ -82,7 +82,6 @@ func (p *Processor) getHashPayload(header types.BlockHeader) ([]byte, error) {
func serializeNewRuneEntry(entry *runes.RuneEntry) []byte {
var sb strings.Builder
sb.WriteString("newRuneEntry:")
// nolint:goconst
sb.WriteString("runeId:" + entry.RuneId.String())
sb.WriteString("number:" + strconv.Itoa(int(entry.Number)))
sb.WriteString("divisibility:" + strconv.Itoa(int(entry.Divisibility)))
@@ -94,7 +93,6 @@ func serializeNewRuneEntry(entry *runes.RuneEntry) []byte {
sb.WriteString("terms:")
terms := entry.Terms
if terms.Amount != nil {
// nolint:goconst
sb.WriteString("amount:" + terms.Amount.String())
}
if terms.Cap != nil {

View File

@@ -9,29 +9,27 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/indexers"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/bitcoin/btcclient"
"github.com/gaze-network/indexer-network/modules/runes/datagateway"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/indexer-network/pkg/btcclient"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gaze-network/indexer-network/pkg/reportingclientv2"
"github.com/gaze-network/uint128"
"github.com/samber/lo"
)
// Make sure to implement the Bitcoin Processor interface
var _ indexer.Processor[*types.Block] = (*Processor)(nil)
var _ indexers.BitcoinProcessor = (*Processor)(nil)
type Processor struct {
runesDg datagateway.RunesDataGateway
indexerInfoDg datagateway.IndexerInfoDataGateway
bitcoinClient btcclient.Contract
network common.Network
reportingClient *reportingclient.ReportingClient
cleanupFuncs []func(context.Context) error
runesDg datagateway.RunesDataGateway
indexerInfoDg datagateway.IndexerInfoDataGateway
bitcoinClient btcclient.Contract
bitcoinDataSource indexers.BitcoinDatasource
network common.Network
reportingClient *reportingclientv2.ReportingClient
newRuneEntries map[runes.RuneId]*runes.RuneEntry
newRuneEntryStates map[runes.RuneId]*runes.RuneEntry
@@ -41,14 +39,14 @@ type Processor struct {
newRuneTxs []*entity.RuneTransaction
}
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, network common.Network, reportingClient *reportingclient.ReportingClient, cleanupFuncs []func(context.Context) error) *Processor {
func NewProcessor(runesDg datagateway.RunesDataGateway, indexerInfoDg datagateway.IndexerInfoDataGateway, bitcoinClient btcclient.Contract, bitcoinDataSource indexers.BitcoinDatasource, network common.Network, reportingClient *reportingclientv2.ReportingClient) *Processor {
return &Processor{
runesDg: runesDg,
indexerInfoDg: indexerInfoDg,
bitcoinClient: bitcoinClient,
bitcoinDataSource: bitcoinDataSource,
network: network,
reportingClient: reportingClient,
cleanupFuncs: cleanupFuncs,
newRuneEntries: make(map[runes.RuneId]*runes.RuneEntry),
newRuneEntryStates: make(map[runes.RuneId]*runes.RuneEntry),
newOutPointBalances: make(map[wire.OutPoint][]*entity.OutPointBalance),
@@ -74,7 +72,7 @@ func (p *Processor) VerifyStates(ctx context.Context) error {
}
}
if p.reportingClient != nil {
if err := p.reportingClient.SubmitNodeReport(ctx, "runes", p.network); err != nil {
if err := p.reportingClient.SubmitNodeReport(ctx, "runes", p.network, Version); err != nil {
return errors.Wrap(err, "failed to submit node report")
}
}
@@ -99,6 +97,7 @@ func (p *Processor) ensureValidState(ctx context.Context) error {
return errors.Wrapf(errs.ConflictSetting, "db version mismatch: current version is %d. Please upgrade to version %d", indexerState.DBVersion, DBVersion)
}
if indexerState.EventHashVersion != EventHashVersion {
// TODO: automate reset db instead of returning error
return errors.Wrapf(errs.ConflictSetting, "event version mismatch: current version is %d. Please reset rune's db first.", indexerState.EventHashVersion, EventHashVersion)
}
}
@@ -159,7 +158,7 @@ func (p *Processor) ensureGenesisRune(ctx context.Context) error {
}
func (p *Processor) Name() string {
return "runes"
return "Runes"
}
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
@@ -193,10 +192,7 @@ func (p *Processor) RevertData(ctx context.Context, from int64) error {
}
defer func() {
if err := runesDgTx.Rollback(ctx); err != nil {
logger.WarnContext(ctx, "failed to rollback transaction",
slogx.Error(err),
slogx.String("event", "rollback_runes_revert"),
)
logger.ErrorContext(ctx, "failed to rollback transaction", err)
}
}()
@@ -230,13 +226,3 @@ func (p *Processor) RevertData(ctx context.Context, from int64) error {
}
return nil
}
func (p *Processor) Shutdown(ctx context.Context) error {
var errs []error
for _, cleanup := range p.cleanupFuncs {
if err := cleanup(ctx); err != nil {
errs = append(errs, err)
}
}
return errors.WithStack(errors.Join(errs...))
}

View File

@@ -18,27 +18,24 @@ import (
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gaze-network/indexer-network/pkg/reportingclientv2"
"github.com/gaze-network/uint128"
"github.com/samber/lo"
)
func (p *Processor) Process(ctx context.Context, blocks []*types.Block) error {
for _, block := range blocks {
ctx := logger.WithContext(ctx, slog.Int64("height", block.Header.Height))
logger.DebugContext(ctx, "Processing new block", slog.Int("txs", len(block.Transactions)))
ctx := logger.WithContext(ctx, slog.Int("block_height", int(block.Header.Height)))
logger.DebugContext(ctx, "[RunesProcessor] Processing block", slog.Int("txs", len(block.Transactions)))
for _, tx := range block.Transactions {
if err := p.processTx(ctx, tx, block.Header); err != nil {
return errors.Wrap(err, "failed to process tx")
}
}
if err := p.flushBlock(ctx, block.Header); err != nil {
return errors.Wrap(err, "failed to flush block")
}
logger.DebugContext(ctx, "Inserted new block")
}
return nil
}
@@ -69,6 +66,13 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
for runeId, balance := range balances {
unallocated[runeId] = unallocated[runeId].Add(balance.Amount)
p.newSpendOutPoints = append(p.newSpendOutPoints, balance.OutPoint)
logger.DebugContext(ctx, "[RunesProcessor] Found runes in tx input",
slogx.Any("runeId", runeId),
slogx.Stringer("amount", balance.Amount),
slogx.Stringer("txHash", balance.OutPoint.Hash),
slog.Int("txOutIndex", int(balance.OutPoint.Index)),
slog.Int("blockHeight", int(tx.BlockHeight)),
)
}
}
@@ -88,6 +92,13 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
}
allocated[output][runeId] = allocated[output][runeId].Add(amount)
unallocated[runeId] = unallocated[runeId].Sub(amount)
logger.DebugContext(ctx, "[RunesProcessor] Allocated runes to tx output",
slogx.Any("runeId", runeId),
slogx.Stringer("amount", amount),
slog.Int("output", output),
slogx.Stringer("txHash", tx.TxHash),
slog.Int("blockHeight", int(tx.BlockHeight)),
)
}
mints := make(map[runes.RuneId]uint128.Uint128)
@@ -120,6 +131,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
if !premine.IsZero() {
unallocated[etchedRuneId] = unallocated[etchedRuneId].Add(premine)
mints[etchedRuneId] = mints[etchedRuneId].Add(premine)
logger.DebugContext(ctx, "[RunesProcessor] Minted premine", slogx.Any("runeId", etchedRuneId), slogx.Stringer("amount", premine))
}
}
@@ -146,7 +158,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)
}
}
@@ -194,6 +206,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
// all input runes and minted runes in a tx with cenotaph are burned
for runeId, amount := range unallocated {
burns[runeId] = burns[runeId].Add(amount)
logger.DebugContext(ctx, "[RunesProcessor] Burned runes in cenotaph", slogx.Any("runeId", runeId), slogx.Stringer("amount", amount))
}
} else {
// assign all un-allocated runes to the default output (pointer), or the first non
@@ -223,6 +236,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
// if pointer is still nil, then no output is available. Burn all unallocated runes.
for runeId, amount := range unallocated {
burns[runeId] = burns[runeId].Add(amount)
logger.DebugContext(ctx, "[RunesProcessor] Burned runes to no output", slogx.Any("runeId", runeId), slogx.Stringer("amount", amount))
}
}
}
@@ -233,6 +247,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
// burn all allocated runes to OP_RETURN outputs
for runeId, amount := range balances {
burns[runeId] = burns[runeId].Add(amount)
logger.DebugContext(ctx, "[RunesProcessor] Burned runes to OP_RETURN output", slogx.Any("runeId", runeId), slogx.Stringer("amount", amount))
}
continue
}
@@ -301,6 +316,7 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
}
}
p.newRuneTxs = append(p.newRuneTxs, &runeTx)
logger.DebugContext(ctx, "[RunesProcessor] created RuneTransaction", slogx.Any("runeTx", runeTx))
return nil
}
@@ -397,6 +413,7 @@ func (p *Processor) mint(ctx context.Context, runeId runes.RuneId, blockHeader t
if err := p.incrementMintCount(ctx, runeId, blockHeader); err != nil {
return uint128.Zero, errors.Wrap(err, "failed to increment mint count")
}
logger.DebugContext(ctx, "[RunesProcessor] Minted rune", slogx.Any("runeId", runeId), slogx.Stringer("amount", amount), slogx.Stringer("mintCount", runeEntry.Mints), slogx.Stringer("cap", lo.FromPtr(runeEntry.Terms.Cap)))
return amount, nil
}
@@ -408,9 +425,11 @@ func (p *Processor) getEtchedRune(ctx context.Context, tx *types.Transaction, ru
if rune != nil {
minimumRune := runes.MinimumRuneAtHeight(p.network, uint64(tx.BlockHeight))
if rune.Cmp(minimumRune) < 0 {
logger.DebugContext(ctx, "invalid etching: rune is lower than minimum rune at this height", slogx.Any("rune", rune), slogx.Any("minimumRune", minimumRune))
return nil, runes.RuneId{}, runes.Rune{}, nil
}
if rune.IsReserved() {
logger.DebugContext(ctx, "invalid etching: rune is reserved", slogx.Any("rune", rune))
return nil, runes.RuneId{}, runes.Rune{}, nil
}
@@ -419,6 +438,7 @@ func (p *Processor) getEtchedRune(ctx context.Context, tx *types.Transaction, ru
return nil, runes.RuneId{}, runes.Rune{}, errors.Wrap(err, "error during check rune existence")
}
if ok {
logger.DebugContext(ctx, "invalid etching: rune already exists", slogx.Any("rune", rune))
return nil, runes.RuneId{}, runes.Rune{}, nil
}
@@ -428,6 +448,7 @@ func (p *Processor) getEtchedRune(ctx context.Context, tx *types.Transaction, ru
return nil, runes.RuneId{}, runes.Rune{}, errors.Wrap(err, "error during check tx commits to rune")
}
if !commit {
logger.DebugContext(ctx, "invalid etching: tx does not commit to the rune", slogx.Any("rune", rune))
return nil, runes.RuneId{}, runes.Rune{}, nil
}
} else {
@@ -443,7 +464,7 @@ func (p *Processor) getEtchedRune(ctx context.Context, tx *types.Transaction, ru
func (p *Processor) txCommitsToRune(ctx context.Context, tx *types.Transaction, rune runes.Rune) (bool, error) {
commitment := rune.Commitment()
for i, txIn := range tx.TxIn {
for _, txIn := range tx.TxIn {
tapscript, ok := extractTapScript(txIn.Witness)
if !ok {
continue
@@ -466,12 +487,13 @@ 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
}
if err != nil {
return false, errors.Wrapf(err, "can't get previous txout for txin `%v:%v`", tx.TxHash.String(), i)
logger.ErrorContext(ctx, "failed to get pk script at out point", err)
continue
}
pkScript := prevTx.TxOut[txIn.PreviousOutIndex].PkScript
// input utxo must be P2TR
@@ -479,7 +501,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
}
@@ -554,6 +576,7 @@ func (p *Processor) createRuneEntry(ctx context.Context, runestone *runes.Runest
}
p.newRuneEntries[runeId] = runeEntry
p.newRuneEntryStates[runeId] = runeEntry
logger.DebugContext(ctx, "[RunesProcessor] created RuneEntry", slogx.Any("runeEntry", runeEntry))
return nil
}
@@ -607,6 +630,7 @@ func (p *Processor) incrementBurnedAmount(ctx context.Context, burned map[runes.
continue
}
runeEntry.BurnedAmount = runeEntry.BurnedAmount.Add(amount)
logger.DebugContext(ctx, "[RunesProcessor] burned amount incremented", slogx.Any("runeId", runeId), slogx.Any("amount", amount))
p.newRuneEntryStates[runeId] = runeEntry
}
return nil
@@ -674,10 +698,7 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
}
defer func() {
if err := runesDgTx.Rollback(ctx); err != nil {
logger.WarnContext(ctx, "failed to rollback transaction",
slogx.Error(err),
slogx.String("event", "rollback_runes_insertion"),
)
logger.ErrorContext(ctx, "[RunesProcessor] failed to rollback runes tx", err)
}
}()
@@ -789,7 +810,7 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
// submit event to reporting system
if p.reportingClient != nil {
if err := p.reportingClient.SubmitBlockReport(ctx, reportingclient.SubmitBlockReportPayload{
if err := p.reportingClient.SubmitBlockReport(ctx, reportingclientv2.SubmitBlockReportPayloadData{
Type: "runes",
ClientVersion: Version,
DBVersion: DBVersion,
@@ -803,5 +824,6 @@ func (p *Processor) flushBlock(ctx context.Context, blockHeader types.BlockHeade
return errors.Wrap(err, "failed to submit block report")
}
}
logger.InfoContext(ctx, "[RunesProcessor] block flushed")
return nil
}

View File

@@ -631,39 +631,13 @@ func (q *Queries) GetRuneIdFromRune(ctx context.Context, rune string) (string, e
return rune_id, 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 (
$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.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 (
$8 <= runes_transactions.block_height AND runes_transactions.block_height <= $9
)
ORDER BY runes_transactions.block_height DESC LIMIT 10000
const getRuneTransactionsByHeight = `-- name: GetRuneTransactionsByHeight :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 runes_transactions.block_height = $1
`
type GetRuneTransactionsParams struct {
FilterPkScript bool
PkScriptParam []byte
FilterRuneID bool
RuneIDParam []byte
RuneID []byte
RuneIDBlockHeight int32
RuneIDTxIndex int32
FromBlock int32
ToBlock int32
}
type GetRuneTransactionsRow struct {
type GetRuneTransactionsByHeightRow struct {
Hash string
BlockHeight int32
Index int32
@@ -696,25 +670,15 @@ type GetRuneTransactionsRow struct {
Flaws pgtype.Int4
}
func (q *Queries) GetRuneTransactions(ctx context.Context, arg GetRuneTransactionsParams) ([]GetRuneTransactionsRow, error) {
rows, err := q.db.Query(ctx, getRuneTransactions,
arg.FilterPkScript,
arg.PkScriptParam,
arg.FilterRuneID,
arg.RuneIDParam,
arg.RuneID,
arg.RuneIDBlockHeight,
arg.RuneIDTxIndex,
arg.FromBlock,
arg.ToBlock,
)
func (q *Queries) GetRuneTransactionsByHeight(ctx context.Context, blockHeight int32) ([]GetRuneTransactionsByHeightRow, error) {
rows, err := q.db.Query(ctx, getRuneTransactionsByHeight, blockHeight)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRuneTransactionsRow
var items []GetRuneTransactionsByHeightRow
for rows.Next() {
var i GetRuneTransactionsRow
var i GetRuneTransactionsByHeightRow
if err := rows.Scan(
&i.Hash,
&i.BlockHeight,

View File

@@ -306,7 +306,7 @@ func mapRuneTransactionTypeToParams(src entity.RuneTransaction) (gen.CreateRuneT
}, runestoneParams, nil
}
func extractModelRuneTxAndRunestone(src gen.GetRuneTransactionsRow) (gen.RunesTransaction, *gen.RunesRunestone, error) {
func extractModelRuneTxAndRunestone(src gen.GetRuneTransactionsByHeightRow) (gen.RunesTransaction, *gen.RunesRunestone, error) {
var runestone *gen.RunesRunestone
if src.TxHash.Valid {
// these fields should never be null

View File

@@ -3,7 +3,6 @@ package postgres
import (
"context"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@@ -62,22 +61,8 @@ func (r *Repository) GetIndexedBlockByHeight(ctx context.Context, height int64)
return indexedBlock, nil
}
func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) {
pkScriptParam := []byte(fmt.Sprintf(`[{"pkScript":"%s"}]`, hex.EncodeToString(pkScript)))
runeIdParam := []byte(fmt.Sprintf(`[{"runeId":"%s"}]`, runeId.String()))
rows, err := r.queries.GetRuneTransactions(ctx, gen.GetRuneTransactionsParams{
FilterPkScript: pkScript != nil,
PkScriptParam: pkScriptParam,
FilterRuneID: runeId != runes.RuneId{},
RuneIDParam: runeIdParam,
RuneID: []byte(runeId.String()),
RuneIDBlockHeight: int32(runeId.BlockHeight),
RuneIDTxIndex: int32(runeId.TxIndex),
FromBlock: int32(fromBlock),
ToBlock: int32(toBlock),
})
func (r *Repository) GetRuneTransactionsByHeight(ctx context.Context, height uint64) ([]*entity.RuneTransaction, error) {
rows, err := r.queries.GetRuneTransactionsByHeight(ctx, int32(height))
if err != nil {
return nil, errors.Wrap(err, "error during query")
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/datagateway"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx"
)
var ErrTxAlreadyExists = errors.New("Transaction already exists. Call Commit() or Rollback() first.")
@@ -55,7 +55,7 @@ func (r *Repository) Rollback(ctx context.Context) error {
return errors.Wrap(err, "failed to rollback transaction")
}
if err == nil {
logger.DebugContext(ctx, "rolled back transaction")
logger.InfoContext(ctx, "rolled back transaction")
}
r.tx = nil
return nil

View File

@@ -1,93 +0,0 @@
package runes
import (
"context"
"strings"
"github.com/btcsuite/btcd/rpcclient"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/core/datasources"
"github.com/gaze-network/indexer-network/core/indexer"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/internal/config"
"github.com/gaze-network/indexer-network/internal/postgres"
runesapi "github.com/gaze-network/indexer-network/modules/runes/api"
runesdatagateway "github.com/gaze-network/indexer-network/modules/runes/datagateway"
runespostgres "github.com/gaze-network/indexer-network/modules/runes/repository/postgres"
runesusecase "github.com/gaze-network/indexer-network/modules/runes/usecase"
"github.com/gaze-network/indexer-network/pkg/btcclient"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/reportingclient"
"github.com/gofiber/fiber/v2"
"github.com/samber/do/v2"
"github.com/samber/lo"
)
func New(injector do.Injector) (indexer.IndexerWorker, error) {
ctx := do.MustInvoke[context.Context](injector)
conf := do.MustInvoke[config.Config](injector)
reportingClient := do.MustInvoke[*reportingclient.ReportingClient](injector)
var (
runesDg runesdatagateway.RunesDataGateway
indexerInfoDg runesdatagateway.IndexerInfoDataGateway
)
var cleanupFuncs []func(context.Context) error
switch strings.ToLower(conf.Modules.Runes.Database) {
case "postgresql", "postgres", "pg":
pg, err := postgres.NewPool(ctx, conf.Modules.Runes.Postgres)
if err != nil {
if errors.Is(err, errs.InvalidArgument) {
return nil, errors.Wrap(err, "Invalid Postgres configuration for indexer")
}
return nil, errors.Wrap(err, "can't create Postgres connection pool")
}
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
pg.Close()
return nil
})
runesRepo := runespostgres.NewRepository(pg)
runesDg = runesRepo
indexerInfoDg = runesRepo
default:
return nil, errors.Wrapf(errs.Unsupported, "%q database for indexer is not supported", conf.Modules.Runes.Database)
}
var bitcoinDatasource datasources.Datasource[*types.Block]
var bitcoinClient btcclient.Contract
switch strings.ToLower(conf.Modules.Runes.Datasource) {
case "bitcoin-node":
btcClient := do.MustInvoke[*rpcclient.Client](injector)
bitcoinNodeDatasource := datasources.NewBitcoinNode(btcClient)
bitcoinDatasource = bitcoinNodeDatasource
bitcoinClient = bitcoinNodeDatasource
default:
return nil, errors.Wrapf(errs.Unsupported, "%q datasource is not supported", conf.Modules.Runes.Datasource)
}
processor := NewProcessor(runesDg, indexerInfoDg, bitcoinClient, conf.Network, reportingClient, cleanupFuncs)
if err := processor.VerifyStates(ctx); err != nil {
return nil, errors.WithStack(err)
}
// Mount API
apiHandlers := lo.Uniq(conf.Modules.Runes.APIHandlers)
for _, handler := range apiHandlers {
switch handler { // TODO: support more handlers (e.g. gRPC)
case "http":
httpServer := do.MustInvoke[*fiber.App](injector)
runesUsecase := runesusecase.New(runesDg, bitcoinClient)
runesHTTPHandler := runesapi.NewHTTPHandler(conf.Network, runesUsecase)
if err := runesHTTPHandler.Mount(httpServer); err != nil {
return nil, errors.Wrap(err, "can't mount Runes API")
}
logger.InfoContext(ctx, "Mounted HTTP handler")
default:
return nil, errors.Wrapf(errs.Unsupported, "%q API handler is not supported", handler)
}
}
indexer := indexer.New(processor, bitcoinDatasource)
return indexer, nil
}

View File

@@ -117,8 +117,7 @@ func (e RuneEntry) Supply() (uint128.Uint128, error) {
}
func (e RuneEntry) MintedAmount() (uint128.Uint128, error) {
terms := lo.FromPtr(e.Terms)
amount, overflow := e.Mints.MulOverflow(lo.FromPtr(terms.Amount))
amount, overflow := e.Mints.MulOverflow(lo.FromPtr(e.Terms.Amount))
if overflow {
return uint128.Uint128{}, errors.WithStack(errs.OverflowUint128)
}

View File

@@ -69,26 +69,8 @@ func ParseTag(input interface{}) (Tag, error) {
return input, nil
case uint128.Uint128:
return Tag(input), nil
case int:
return Tag(uint128.From64(uint64(input))), nil
case int8:
return Tag(uint128.From64(uint64(input))), nil
case int16:
return Tag(uint128.From64(uint64(input))), nil
case int32:
return Tag(uint128.From64(uint64(input))), nil
case int64:
return Tag(uint128.From64(uint64(input))), nil
case uint:
return Tag(uint128.From64(uint64(input))), nil
case uint8:
return Tag(uint128.From64(uint64(input))), nil
case uint16:
return Tag(uint128.From64(uint64(input))), nil
case uint32:
return Tag(uint128.From64(uint64(input))), nil
case uint64:
return Tag(uint128.From64(input)), nil
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return Tag(uint128.From64(input.(uint64))), nil
case big.Int:
u128, err := uint128.FromBig(&input)
if err != nil {

View File

@@ -5,11 +5,10 @@ import (
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
)
func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, runeId runes.RuneId, fromBlock, toBlock uint64) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactions(ctx, pkScript, runeId, fromBlock, toBlock)
func (u *Usecase) GetTransactionsByHeight(ctx context.Context, height uint64) ([]*entity.RuneTransaction, error) {
txs, err := u.runesDg.GetRuneTransactionsByHeight(ctx, height)
if err != nil {
return nil, errors.Wrap(err, "error during GetTransactionsByHeight")
}

View File

@@ -1,8 +1,8 @@
package usecase
import (
"github.com/gaze-network/indexer-network/modules/bitcoin/btcclient"
"github.com/gaze-network/indexer-network/modules/runes/datagateway"
"github.com/gaze-network/indexer-network/pkg/btcclient"
)
type Usecase struct {

View File

@@ -1,99 +0,0 @@
package automaxprocs
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"github.com/Cleverse/go-utilities/utils"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"go.uber.org/automaxprocs/maxprocs"
)
var (
// undo is the undo function returned by maxprocs.Set
undo func()
// autoMaxProcs is the value of GOMAXPROCS set by automaxprocs.
// will be -1 if `automaxprocs` is not initialized.
autoMaxProcs = -1
// initialMaxProcs is the initial value of GOMAXPROCS.
initialMaxProcs = Current()
)
func Init() error {
logger := logger.With(
slogx.String("package", "automaxprocs"),
slogx.String("event", "set_gomaxprocs"),
slogx.Int("prev_maxprocs", initialMaxProcs),
)
// Create a logger function for `maxprocs.Set`.
setMaxProcLogger := func(format string, v ...any) {
fields := make([]slog.Attr, 0, 1)
// `maxprocs.Set` will always pass current GOMAXPROCS value to logger.
// except when calling `undo` function, it will not pass any value.
if val, ok := utils.Optional(v); ok {
// if `GOMAXPROCS` environment variable is set, then `automaxprocs` will honor it.
if _, exists := os.LookupEnv("GOMAXPROCS"); exists {
val = Current()
}
// add logging field for `set_maxprocs` value if it's present in integer value.
if setmaxprocs, ok := val.(int); ok {
fields = append(fields, slogx.Int("set_maxprocs", setmaxprocs))
}
}
logger.LogAttrs(context.Background(), slog.LevelInfo, fmt.Sprintf(format, v...), fields...)
}
// Set GOMAXPROCS to match the Linux container CPU quota (if any), returning
// any error encountered and an undo function.
//
// Set is a no-op on non-Linux systems and in Linux environments without a
// configured CPU quota.
revert, err := maxprocs.Set(maxprocs.Logger(setMaxProcLogger), maxprocs.Min(1))
if err != nil {
return errors.WithStack(err)
}
// set the result of `maxprocs.Set` to global variable.
autoMaxProcs = Current()
undo = revert
return nil
}
// Undo restores GOMAXPROCS to its previous value.
// or revert to initial value if `automaxprocs` is not initialized.
//
// returns the current GOMAXPROCS value.
func Undo() int {
if undo != nil {
undo()
return Current()
}
runtime.GOMAXPROCS(initialMaxProcs)
return initialMaxProcs
}
// Current returns the current value of GOMAXPROCS.
func Current() int {
return runtime.GOMAXPROCS(0)
}
// Value returns the value of GOMAXPROCS set by automaxprocs.
// returns -1 if `automaxprocs` is not initialized.
func Value() int {
if autoMaxProcs <= 0 {
return -1
}
return autoMaxProcs
}

View File

@@ -1,12 +0,0 @@
package btcclient
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
type Contract interface {
GetRawTransactionAndHeightByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, int64, 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
)

105
pkg/crypto/crypto.go Normal file
View File

@@ -0,0 +1,105 @@
package crypto
import (
"encoding/base64"
"encoding/hex"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
ecies "github.com/ecies/go/v2"
)
type Client struct {
privateKey *btcec.PrivateKey
eciesPrivateKey *ecies.PrivateKey
}
func New(privateKeyStr string) (*Client, error) {
if privateKeyStr != "" {
privateKeyBytes, err := hex.DecodeString(privateKeyStr)
if err != nil {
return nil, errors.Wrap(err, "decode private key")
}
privateKey, _ := btcec.PrivKeyFromBytes(privateKeyBytes)
eciesPrivateKey := ecies.NewPrivateKeyFromBytes(privateKeyBytes)
return &Client{
privateKey: privateKey,
eciesPrivateKey: eciesPrivateKey,
}, nil
}
return &Client{}, nil
}
func (c *Client) PublicKey() string {
return hex.EncodeToString(c.privateKey.PubKey().SerializeCompressed())
}
func (c *Client) WIF(params *chaincfg.Params) (string, error) {
wif, err := btcutil.NewWIF(c.privateKey, params, true)
if err != nil {
return "", errors.Wrap(err, "wif convert failed")
}
return wif.String(), nil
}
func (c *Client) Sign(message string) string {
messageHash := chainhash.DoubleHashB([]byte(message))
signature := ecdsa.Sign(c.privateKey, messageHash)
return hex.EncodeToString(signature.Serialize())
}
func (c *Client) Verify(message, sigStr, pubKeyStr string) (bool, error) {
sigBytes, err := hex.DecodeString(sigStr)
if err != nil {
return false, errors.Wrap(err, "signature decode")
}
pubBytes, err := hex.DecodeString(pubKeyStr)
if err != nil {
return false, errors.Wrap(err, "pubkey decode")
}
pubKey, err := btcec.ParsePubKey(pubBytes)
if err != nil {
return false, errors.Wrap(err, "pubkey parse")
}
messageHash := chainhash.DoubleHashB([]byte(message))
signature, err := ecdsa.ParseSignature(sigBytes)
if err != nil {
return false, errors.Wrap(err, "signature parse")
}
return signature.Verify(messageHash, pubKey), nil
}
func (c *Client) Encrypt(message, pubKeyStr string) (string, error) {
pubKey, err := ecies.NewPublicKeyFromHex(pubKeyStr)
if err != nil {
return "", errors.Wrap(err, "parse pubkey")
}
ciphertext, err := ecies.Encrypt(pubKey, []byte(message))
if err != nil {
return "", errors.Wrap(err, "encrypt message")
}
ciphertextStr := base64.StdEncoding.EncodeToString(ciphertext)
return ciphertextStr, nil
}
func (c *Client) Decrypt(ciphertextStr string) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextStr)
if err != nil {
return "", errors.Wrap(err, "decode ciphertext")
}
plaintext, err := ecies.Decrypt(c.eciesPrivateKey, ciphertext)
if err != nil {
return "", errors.Wrap(err, "decrypt")
}
return string(plaintext), nil
}

65
pkg/crypto/crypto_test.go Normal file
View File

@@ -0,0 +1,65 @@
package crypto
import (
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/stretchr/testify/assert"
)
// Key for test only. DO NOT USE IN PRODUCTION
const (
privateKeyStr = "ce9c2fd75623e82a83ed743518ec7749f6f355f7301dd432400b087717fed2f2"
mainnetKey = "L49LKamtrPZxty5TG7jaFPHMRZbrvAr4Dvn5BHGdvmvbcTDNAbZj"
pubKeyStr = "0251e2dfcdeea17cc9726e4be0855cd0bae19e64f3e247b10760cd76851e7df47e"
)
func TestEncryptDecrypt(t *testing.T) {
plaintext := "hello world"
privClient, err := New(privateKeyStr)
assert.NoError(t, err)
pubClient, err := New("")
assert.NoError(t, err)
ciphertext, err := pubClient.Encrypt(plaintext, pubKeyStr)
assert.NoError(t, err)
decrypted, err := privClient.Decrypt(ciphertext)
assert.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
func TestSignVerify(t *testing.T) {
plaintext := "hello world"
invalidSignature := "3044022066504a82e2bc23167214e05497a1ca957add9cacc078aa69f5417079a4d56f0002206b215920b046c779d4a58d4029c26dbadcaf6d3c884b3463f44e70ef9146c1cd"
privClient, err := New(privateKeyStr)
assert.NoError(t, err)
pubClient, err := New("")
assert.NoError(t, err)
signature := privClient.Sign(plaintext)
println(signature)
verified, err := pubClient.Verify(plaintext, signature, pubKeyStr)
assert.NoError(t, err)
assert.True(t, verified)
verified, err = pubClient.Verify(plaintext, invalidSignature, pubKeyStr)
assert.NoError(t, err)
assert.False(t, verified)
}
func TestWIF(t *testing.T) {
privClient, err := New(privateKeyStr)
assert.NoError(t, err)
wifPrivKey, err := privClient.WIF(&chaincfg.MainNetParams)
assert.NoError(t, err)
assert.Equal(t, wifPrivKey, mainnetKey)
}

View File

@@ -10,28 +10,19 @@ 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"),
slogx.Error(err),
)
return errors.WithStack(ctx.Status(http.StatusInternalServerError).JSON(fiber.Map{
logger.ErrorContext(ctx.UserContext(), "unhandled error", slogx.Error(err))
return errors.WithStack(ctx.Status(http.StatusInternalServerError).JSON(map[string]any{
"error": "Internal Server Error",
}))
}

View File

@@ -5,10 +5,10 @@ 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/pkg/logger"
"github.com/valyala/fasthttp"
@@ -23,13 +23,12 @@ 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 {
if _, err := url.Parse(baseURL); err != nil {
return nil, errors.Wrap(err, "can't parse base url")
}
var cf Config
@@ -40,7 +39,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,21 +59,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)
}
switch strings.ToLower(string(r.Header.ContentType())) {
case "application/json", "application/json; charset=utf-8":
if err := json.Unmarshal(body, out); err != nil {
return errors.Wrapf(err, "can't unmarshal json body from %s, %q", r.URL, string(body))
}
return nil
case "text/plain", "text/plain; charset=utf-8":
return errors.Errorf("can't unmarshal plain text %q", string(body))
default:
return errors.Errorf("unsupported content type: %s, contents: %v", r.Header.ContentType(), string(r.Body()))
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) {
@@ -87,14 +76,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)
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")
@@ -126,12 +110,11 @@ 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())),
)
}
logger.InfoContext(ctx, "Finished make request", slog.String("package", "httpclient"))
logger.Info("Finished make request")
}
fasthttp.ReleaseResponse(resp)
@@ -150,12 +133,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

@@ -1,15 +0,0 @@
package logger
import (
"log/slog"
)
func durationToMsAttrReplacer(groups []string, attr slog.Attr) slog.Attr {
if attr.Value.Kind() == slog.KindDuration {
return slog.Attr{
Key: attr.Key,
Value: slog.Int64Value(attr.Value.Duration().Milliseconds()),
}
}
return attr
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/gaze-network/indexer-network/pkg/stacktrace"
)
func middlewareErrorStackTrace() middleware {
func middlewareError() middleware {
return func(next handleFunc) handleFunc {
return func(ctx context.Context, rec slog.Record) error {
rec.Attrs(func(attr slog.Attr) bool {

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 (
@@ -133,7 +133,9 @@ var (
}
// Default Middlewares
defaultMiddleware = []middleware{}
defaultMiddleware = []middleware{
middlewareError(),
}
)
// Init initializes global logger and slog logger with given configuration.
@@ -145,14 +147,12 @@ func Init(cfg Config) error {
Level: lvl,
ReplaceAttr: attrReplacerChain(defaultAttrReplacers...),
}
middlewares = append([]middleware{}, defaultMiddleware...)
)
lvl.Set(slog.LevelInfo)
if cfg.Debug {
lvl.Set(slog.LevelDebug)
options.AddSource = true
middlewares = append(middlewares, middlewareErrorStackTrace())
}
switch strings.ToLower(cfg.Output) {
@@ -165,7 +165,7 @@ func Init(cfg Config) error {
handler = slog.NewTextHandler(os.Stdout, options)
}
logger = slog.New(newChainHandlers(handler, middlewares...))
logger = slog.New(newChainHandlers(handler, defaultMiddleware...))
slog.SetDefault(logger)
return nil
}

View File

@@ -7,18 +7,12 @@ import (
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
)
// NewGCPHandler returns a new GCP handler.
// The handler writes logs to the os.Stdout and
// replaces the default attribute keys/values with the GCP logging attribute keys/values
//
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
func NewGCPHandler(opts *slog.HandlerOptions) slog.Handler {
return slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: opts.Level,
ReplaceAttr: attrReplacerChain(
GCPAttrReplacer,
durationToMsAttrReplacer,
opts.ReplaceAttr,
),
})

View File

@@ -1,7 +0,0 @@
# Proxies IP Range Resources
- Cloudflare - https://www.cloudflare.com/ips/
- GCP Load Balancer - https://cloud.google.com/load-balancing/docs/health-check-concepts#ip-ranges
- GCP Compute Engine, Customer-usable external IP address ranges - https://www.gstatic.com/ipranges/cloud.json
- Other GCP Services - https://cloud.google.com/compute/docs/faq#networking
- Other Resources - https://github.com/lord-alfred/ipranges

View File

@@ -1,21 +0,0 @@
package requestcontext
// requestcontextError implements error interface
var _ error = requestcontextError{}
type requestcontextError struct {
err error
status int
message string
}
func (r requestcontextError) Error() string {
if r.err != nil {
return r.err.Error()
}
return r.message
}
func (r requestcontextError) Unwrap() error {
return r.err
}

View File

@@ -1,44 +0,0 @@
package requestcontext
import (
"context"
"log/slog"
"net/http"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gofiber/fiber/v2"
)
type Response struct {
Result any `json:"result"`
Error string `json:"error,omitempty"`
}
type Option func(ctx context.Context, c *fiber.Ctx) (context.Context, error)
func New(opts ...Option) fiber.Handler {
return func(c *fiber.Ctx) error {
var err error
ctx := c.UserContext()
for i, opt := range opts {
ctx, err = opt(ctx, c)
if err != nil {
rErr := requestcontextError{}
if errors.As(err, &rErr) {
return c.Status(rErr.status).JSON(Response{Error: rErr.message})
}
logger.ErrorContext(ctx, "failed to extract request context",
err,
slog.String("event", "requestcontext/error"),
slog.String("module", "requestcontext"),
slog.Int("optionIndex", i),
)
return c.Status(http.StatusInternalServerError).JSON(Response{Error: "internal server error"})
}
}
c.SetUserContext(ctx)
return c.Next()
}
}

View File

@@ -1,150 +0,0 @@
package requestcontext
import (
"context"
"log/slog"
"net"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gofiber/fiber/v2"
)
type clientIPKey struct{}
type WithClientIPConfig struct {
// [Optional] TrustedProxiesIP is a list of all proxies IP ranges that's between the server and the client.
//
// If it's provided, it will walk backwards from the last IP in `X-Forwarded-For` header
// and use first IP that's not trusted proxy(not in the given IP ranges.)
//
// **If you want to use this option, you should provide all of probable proxies IP ranges.**
//
// This is lowest priority.
TrustedProxiesIP []string `env:"TRUSTED_PROXIES_IP" mapstructure:"trusted_proxies_ip"`
// [Optional] TrustedHeader is a header name for getting client IP. (e.g. X-Real-IP, CF-Connecting-IP, etc.)
//
// This is highest priority, it will ignore rest of the options if it's provided.
TrustedHeader string `env:"TRUSTED_HEADER" mapstructure:"trusted_proxies_header"`
// EnableRejectMalformedRequest return 403 Forbidden if the request is from proxies, but can't extract client IP
EnableRejectMalformedRequest bool `env:"ENABLE_REJECT_MALFORMED_REQUEST" envDefault:"false" mapstructure:"enable_reject_malformed_request"`
}
// WithClientIP setup client IP context with XFF Spoofing prevention support.
//
// If request is from proxies, it will use first IP from `X-Forwarded-For` header by default.
func WithClientIP(config WithClientIPConfig) Option {
var trustedProxies trustedProxy
if len(config.TrustedProxiesIP) > 0 {
proxy, err := newTrustedProxy(config.TrustedProxiesIP)
if err != nil {
logger.Panic("Failed to parse trusted proxies", err)
}
trustedProxies = proxy
}
return func(ctx context.Context, c *fiber.Ctx) (context.Context, error) {
// Extract client IP from given header
if config.TrustedHeader != "" {
headerIP := c.Get(config.TrustedHeader)
// validate ip from header
if ip := net.ParseIP(headerIP); ip != nil {
return context.WithValue(ctx, clientIPKey{}, headerIP), nil
}
}
// Extract client IP from XFF header
rawIPs := c.IPs()
ips := parseIPs(rawIPs)
// If the request is directly from client, we can use direct remote IP address
if len(ips) == 0 {
return context.WithValue(ctx, clientIPKey{}, c.IP()), nil
}
// Walk back and find first IP that's not trusted proxy
if len(trustedProxies) > 0 {
for i := len(ips) - 1; i >= 0; i-- {
if !trustedProxies.IsTrusted(ips[i]) {
return context.WithValue(ctx, clientIPKey{}, ips[i].String()), nil
}
}
// If all IPs are trusted proxies, return first IP in XFF header
return context.WithValue(ctx, clientIPKey{}, rawIPs[0]), nil
}
// Finally, if we can't extract client IP, return forbidden
if config.EnableRejectMalformedRequest {
logger.WarnContext(ctx, "IP Spoofing detected, returning 403 Forbidden",
slog.String("event", "requestcontext/ip_spoofing_detected"),
slog.String("module", "requestcontext/with_clientip"),
slog.String("ip", c.IP()),
slog.Any("ips", rawIPs),
)
return nil, requestcontextError{
status: fiber.StatusForbidden,
message: "not allowed to access",
}
}
// Fallback to first IP in XFF header
return context.WithValue(ctx, clientIPKey{}, rawIPs[0]), nil
}
}
// GetClientIP get clientIP from context. If not found, return empty string
//
// Warning: Request context should be setup before using this function
func GetClientIP(ctx context.Context) string {
if ip, ok := ctx.Value(clientIPKey{}).(string); ok {
return ip
}
return ""
}
type trustedProxy []*net.IPNet
// newTrustedProxy create a new trusted proxies instance for preventing IP spoofing (XFF Attacks)
func newTrustedProxy(ranges []string) (trustedProxy, error) {
nets, err := parseCIDRs(ranges)
if err != nil {
return nil, errors.WithStack(err)
}
return trustedProxy(nets), nil
}
func (t trustedProxy) IsTrusted(ip net.IP) bool {
if ip == nil {
return false
}
for _, r := range t {
if r.Contains(ip) {
return true
}
}
return false
}
func parseCIDRs(ranges []string) ([]*net.IPNet, error) {
nets := make([]*net.IPNet, 0, len(ranges))
for _, r := range ranges {
_, ipnet, err := net.ParseCIDR(r)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse CIDR for %q", r)
}
nets = append(nets, ipnet)
}
return nets, nil
}
func parseIPs(ranges []string) []net.IP {
ip := make([]net.IP, 0, len(ranges))
for _, r := range ranges {
ip = append(ip, net.ParseIP(r))
}
return ip
}

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