Compare commits

..

242 Commits

Author SHA1 Message Date
Nicolas Dorier
5147b5f261
bump deps 2026-06-10 10:12:06 +09:00
Nicolas Dorier
6ad4941712
Merge pull request #544 from s373nZ/docker-curl
Add `curl` to Docker image to support health checks
2026-06-02 09:07:36 +09:00
se7enz
98d89f924a
docker: Add curl to Docker image to support health checks 2026-05-26 09:52:03 +02:00
Nicolas Dorier
36e1596b5c
Faster tests 2026-04-24 14:14:09 +09:00
Nicolas Dorier
83e0ab4f68
Relax xpub validation 2026-04-24 12:25:52 +09:00
Nicolas Dorier
c24b47bb03
Deprecate import into RPC 2026-04-24 09:05:55 +09:00
Nicolas Dorier
b9f324b2eb
bump testframework 2026-04-23 15:33:14 +09:00
Nicolas Dorier
e0af3bf493
Fix broken links 2026-04-23 15:23:49 +09:00
Nicolas Dorier
06349f9818
Trying to speedup tests 2026-04-23 14:51:28 +09:00
Nicolas Dorier
c46949ac60
Merge pull request #542 from dgarage/chunk
Add chunk fee and weight to transaction metadata
2026-04-22 18:14:34 +09:00
Nicolas Dorier
5e570a84fb
Add chunk fee and weight to transaction metadata 2026-04-22 18:03:29 +09:00
Nicolas Dorier
060d19f6a8
bump 2026-04-22 11:13:19 +09:00
Nicolas Dorier
b83178be8d
Fix tests 2026-04-22 10:51:31 +09:00
Nicolas Dorier
f72fc4d321
Remove some error during tests 2026-04-22 09:59:01 +09:00
Nicolas Dorier
9242d645b6
Trying to speed up tests 2026-04-22 09:25:43 +09:00
Nicolas Dorier
05df9b5037
Decrease spams in test logs 2026-04-22 09:06:09 +09:00
Nicolas Dorier
456717935c
Fix tests 2026-04-22 09:03:32 +09:00
Nicolas Dorier
98df6b7fdd
Improve error messages for RPC wallet errors 2026-04-22 08:54:33 +09:00
Nicolas Dorier
449568be9a
Bump test to 31.0 2026-04-22 08:47:51 +09:00
Nicolas Dorier
abb5fd3a6c
bump bitcoin core in tests 2026-04-21 21:21:35 +09:00
Nicolas Dorier
deb868b2c0
Add PushNuget.sh 2026-04-21 10:38:46 +09:00
Nicolas Dorier
f0000ceab6
bump libs 2026-04-21 10:30:00 +09:00
Nicolas Dorier
d9cba8b0b5
bump nbt 2026-04-11 09:17:57 +09:00
Nicolas Dorier
becdc3f47d
Fix DOGE getting stuck 2026-04-10 22:33:18 +09:00
Nicolas Dorier
aa462e7af1
bump 2026-04-10 11:28:07 +09:00
Nicolas Dorier
34d6b0dcf3
Potentially fix dogecoin bug, improve logs when node connection fail with exception 2026-04-10 11:27:47 +09:00
Nicolas Dorier
87037e17e1
Improve logs at startup 2026-04-10 10:44:46 +09:00
Nicolas Dorier
c806e9022e
Do not use static logger in tests 2026-04-10 09:44:04 +09:00
Nicolas Dorier
39b1861d7a
Fix test 2026-04-10 09:27:13 +09:00
Nicolas Dorier
a379506b46
Port code to dotnet10.0 style 2026-04-10 09:18:44 +09:00
Nicolas Dorier
68513b4668
Remove message broker leftover 2026-04-10 09:09:44 +09:00
Nicolas Dorier
802945291a
Remove message broker dependencies 2026-04-10 08:55:28 +09:00
Nicolas Dorier
3756b54138
Fix compatibility with core 31.0 2026-04-10 08:17:05 +09:00
Nicolas Dorier
9eeaf71195
bump 2026-04-03 11:05:43 +09:00
Nicolas Dorier
0e4222a1f2
Merge pull request #539 from Unknown-Kush/master
Add Pepecoin
2026-04-03 11:04:13 +09:00
Unknown-Kush
34831b2d85 Add Pepecoin support 2026-04-02 09:51:55 -04:00
Unknown-Kush
aba8cdc8c6 bump NBitcoin.Altcoins 2026-04-02 09:24:52 -04:00
Nicolas Dorier
ba76d3b059
bump dockerfile 2026-04-01 10:07:47 +09:00
Nicolas Dorier
26b3dddc28
bump nbx 2026-03-11 09:54:42 +09:00
Nicolas Dorier
1a38ffa497
Bump libraries 2026-03-11 09:54:42 +09:00
Nicolas Dorier
2fb63ae18e
Merge pull request #537 from NicolasDorier/retry-flaky-rpc
Retry flaky idempotent RPC requests
2026-03-11 09:44:29 +09:00
Nicolas Dorier
206fd0017f
Fix .net10 mention 2026-03-11 09:42:01 +09:00
Nicolas Dorier
5d78c7034c
Retry flaky idempotent RPC requests 2026-03-11 09:32:27 +09:00
Nicolas Dorier
f226238779
Add publish-docker.sh 2026-03-11 09:15:22 +09:00
Nicolas Dorier
05d8ed054e bump 2025-12-26 11:05:07 +09:00
Nicolas Dorier
4ebd7c4c2f
Update to .NET10.0 and bump libs (#534) 2025-12-20 23:29:30 +09:00
Nicolas Dorier
8670e58923 bump postgres 2025-12-18 16:51:01 +09:00
nicolas.dorier
5ea76f64bb
bump NBitcoin.Testframework 2025-09-09 22:06:10 +09:00
nicolas.dorier
be086282ae
Allow feature tag to version on docker 2025-09-06 11:29:45 +09:00
nicolas.dorier
58bde3c916
bump asp.net 2025-09-06 11:25:36 +09:00
nicolas.dorier
8af448f724
bump 2025-08-27 19:41:21 +09:00
Nicolas Dorier
273333b040
Fix documentation AI slop, allow more format for broadcasting (#529) 2025-08-27 11:40:53 +01:00
Nicolas Dorier
d03465c240
Fix incorrect doc for GetUTXOs (#530) 2025-08-27 11:40:33 +01:00
nicolas.dorier
58e9bba5a2
Fix: Unable to delete a ADDRESS: item in a group 2025-08-27 19:40:09 +09:00
nicolas.dorier
c58c60c9fa
bump 2025-08-25 10:09:38 +09:00
nicolas.dorier
939a575c51
Fix: Periodic tasks would sometimes stop firing 2025-08-25 10:09:37 +09:00
nicolas.dorier
ca97104831
bump 2025-07-02 12:48:43 +09:00
gruve-p
a51911f4ce
Bump GRS to 29.0 (#517) 2025-07-02 12:32:49 +09:00
Nicolas Dorier
e8316d959b
Add key index to events (#525) 2025-07-02 12:32:33 +09:00
nicolas.dorier
be51a4af50
Add PSBTCoin.HDKeysFor extension method 2025-06-09 17:54:18 +09:00
nicolas.dorier
b7dc5b5c5f
Add PSBT.HDKeysFor for derivation schemes 2025-06-09 17:41:08 +09:00
nicolas.dorier
cdac2c264e
Add overloads to SignAll and GetBalance PSBT 2025-06-09 17:27:20 +09:00
nicolas.dorier
e494a46de9
Bump NBX 2025-06-09 16:56:54 +09:00
nicolas.dorier
10ac6d65d8
Bump NBitcoin 2025-06-09 16:56:02 +09:00
nicolas.dorier
7f84ef983c
Fix doc anchor 2025-06-02 18:57:15 +09:00
nicolas.dorier
9a784b5cce
Major bump version for NBXplorer.Client 2025-06-02 16:56:43 +09:00
nicolas.dorier
dfe7ca478f
Pass keypath when possible to Derivation 2025-06-02 16:36:10 +09:00
nicolas.dorier
ea4ac907b6
Add convenience overload for GetLineFor 2025-06-02 16:28:23 +09:00
nicolas.dorier
fa8bf56623
bump 2025-06-02 11:12:14 +09:00
Nicolas Dorier
f4640c8c4f
Support miniscript derivation scheme (#523)
* Support miniscript derivation scheme

* Adjustements

* fix
2025-06-02 11:11:28 +09:00
nicolas.dorier
fdc8281e88
Bump NBitcoin 2025-05-15 13:43:36 +09:00
nicolas.dorier
8597067cbe
bump NBitcoin 2025-05-15 10:35:22 +09:00
nicolas.dorier
ae99da7299
Simplify code, fix another bug which can stall sync 2025-04-05 10:53:41 +09:00
nicolas.dorier
4666c13a1b
bump 2025-04-04 23:16:04 +09:00
nicolas.dorier
3118b71004
Fix: NBXplorer stalling every 2000 blocks 2025-04-04 23:07:41 +09:00
nicolas.dorier
d61f3db4fa
bump nbxplorer 2025-04-04 22:28:04 +09:00
nicolas.dorier
10e139c3d1
For liquid blinded addresses returns blindingKey in KeyInformation 2025-04-04 22:22:15 +09:00
nicolas.dorier
206165736a
Add doc for the fullschema 2025-04-02 21:43:58 +09:00
nicolas.dorier
f68c50c4cf
Fix C# comment 2025-03-11 17:23:02 +09:00
Nicolas Dorier
75cbba9427
Get transaction data from peer when possible (#511) 2025-03-06 17:40:09 +09:00
Nicolas Dorier
1916ea7050
Add PSBTVersion to CreatePSBT (#510) 2025-03-05 18:26:38 +09:00
nicolas.dorier
e43031411d
bump nbx 2025-02-17 18:00:30 +09:00
nicolas.dorier
7b7cd2314d
Fix regression from .21 2025-02-09 23:53:44 +09:00
nicolas.dorier
4ce8ef6954
CreatePSBT: explicitFee shouldn't be mutually exclusive with feeRate settings 2025-02-09 23:10:44 +09:00
nicolas.dorier
80f16ce8bc
Allow CreatePSBT.ExplicitChangeAddress to be an hex script 2025-01-30 17:34:29 +09:00
nicolas.dorier
fed67b354f
bump 2025-01-30 17:27:20 +09:00
Nicolas Dorier
da251bdbe0
CreatePSBT accept raw scripts (#509) 2025-01-30 17:27:00 +09:00
nicolas.dorier
11a3564ab4
Fix: Feature property was always null. Exception thrown if PSBTDest.Value is null 2025-01-30 16:18:51 +09:00
nicolas.dorier
e3f7933ad1
Fixup GetTransaction crash in case an unknown entry isn't in the mempool 2025-01-30 15:39:20 +09:00
nicolas.dorier
8b9d5fbde7
Bump, and nicer error message if amount are negative in create PSBT 2025-01-30 15:36:46 +09:00
Nicolas Dorier
916a52b49e
Add fee and mempool information to txs fetched in the mempool (#508) 2025-01-30 14:21:25 +09:00
Nicolas Dorier
2ed4f7850f
Bump Bitcoin Core and NBitcoin (#506) 2025-01-17 00:07:31 +09:00
Nicolas Dorier
a38ad24023
Bump Bitcoin Core in tests (#505) 2025-01-13 12:06:19 +09:00
nicolas.dorier
ab27e4b6ce
bump 2025-01-06 19:09:02 +09:00
Nicolas Dorier
e9a235c149
New route for subscribing to events via websockets (#503) 2025-01-06 19:08:41 +09:00
nicolas.dorier
03c17c046d
Improve doc 2024-12-27 19:15:47 +09:00
nicolas.dorier
530a38030e
Fix: Issue when using custom key templates after scanning utxosets 2024-12-27 16:41:12 +09:00
nicolas.dorier
9d796828f8
Refactoring: Do not use keyPathTemplates directly 2024-12-27 16:31:54 +09:00
Nicolas Dorier
eaeeea22a9
Remove dependencies to keytemplates (#502) 2024-12-27 16:16:12 +09:00
nicolas.dorier
5170fd92e1
Returns KeyPathInformation.Index and reuse queries 2024-12-25 21:48:08 +09:00
nicolas.dorier
e5eaf763e7
Make doc a bit more user-friendly by using default postgres port (Fix #498) 2024-12-19 12:23:59 +09:00
Nicolas Dorier
326d9c5e2b
Fix: Taproot PSBT's outputs were not including internal key and taproot hd information (#495) 2024-12-05 15:20:51 +09:00
Nicolas Dorier
8ea14672dd
Bump dependencies (#494) 2024-12-03 14:40:37 +09:00
nicolas.dorier
4d81c236f5
Remove deps 2024-11-30 10:30:12 +09:00
nicolas.dorier
a2afbd951e
More docs 2024-11-29 14:27:00 +09:00
nicolas.dorier
35b449a49f
Fix build 2024-11-29 14:13:57 +09:00
nicolas.dorier
5ab3da5031
Improve docs, show version on startup 2024-11-29 14:09:03 +09:00
nicolas.dorier
dbfd09e2f9
bump 2024-11-29 10:02:06 +09:00
Nicolas Dorier
24d24f2f25
Fix timeout errors, Fix NBXplorer hanging when exiting (#491) 2024-11-29 10:01:21 +09:00
nicolas.dorier
1eeb0ff3ef
Fixup doc 2024-11-29 10:01:01 +09:00
nicolas.dorier
b31fa111b4
Add doc 2024-11-29 10:00:25 +09:00
nicolas.dorier
fb2691748f
Reword doc 2024-11-28 22:32:44 +09:00
nicolas.dorier
c64dc124b5
Better docs 2024-11-28 22:30:11 +09:00
nicolas.dorier
97214740c1
Avoid crash on useless config parameters (#470) 2024-11-28 22:10:07 +09:00
nicolas.dorier
3e0b2047d3
Avoid crash on useless config parameters (#470) 2024-11-28 21:20:01 +09:00
nicolas.dorier
2fa2ca69a0
bump 2024-11-28 19:27:22 +09:00
nicolas.dorier
f37ef1c2d5
Improve doc for groups 2024-11-28 14:34:05 +09:00
Nicolas Dorier
f165b14c52
GetTransactions can be called with from/to timestamp filter (#490) 2024-11-28 14:25:59 +09:00
Andrew Camilleri
c249b842ef
Import utxo (#465) 2024-11-28 13:38:27 +09:00
Nicolas Dorier
531f28817e
Refactor transaction matching, fix elements (#489)
Co-authored-by: Kukks <evilkukka@gmail.com>
2024-11-28 13:30:08 +09:00
nicolas.dorier
654b103128
Improve doc 2024-11-28 13:14:09 +09:00
José Manuel Nieto
21122b4a1f
Refine endpoints (#488) 2024-11-28 12:29:27 +09:00
José Manuel Nieto
e547b99a47
Add API documentation for /cryptos/{cryptoCode}/rpc (#486)
* Add documentation for /cryptos/{cryptoCode}/rpc

* Fix issues during review

* Improve wording
2024-11-28 12:18:07 +09:00
Nicolas Dorier
88f7d8248a
Refactor matching (#487) 2024-11-26 22:05:48 +09:00
nicolas.dorier
a201004b42
Fix: RPC proxy wasn't supporting requests with string as version 2024-11-26 13:25:57 +09:00
nicolas.dorier
0bf9492e1d
Replace from azure to github pages 2024-11-25 17:34:30 +09:00
nicolas.dorier
8275c6effb
Add index.html 2024-11-25 17:27:34 +09:00
nicolas.dorier
3d746aeff9
Fix some links 2024-11-25 16:24:57 +09:00
nicolas.dorier
8f7371dc47
Make API openapi 3.0 compliant 2024-11-25 16:06:40 +09:00
nicolas.dorier
048eab8625
Document basic auth 2024-11-25 15:55:59 +09:00
nicolas.dorier
d4068e7dec
Add doc for generate hot wallet 2024-11-25 13:09:55 +09:00
nicolas.dorier
1352c8dcdf
Add missing Create PSBT 2024-11-25 12:47:16 +09:00
nicolas.dorier
e2767a85c5
Adjustement docs 2024-11-25 12:13:38 +09:00
nicolas.dorier
09d56b22be
Cleanup repetitions 2024-11-25 12:09:53 +09:00
nicolas.dorier
1ee089836d
Add missing routes to groups 2024-11-25 12:04:00 +09:00
nicolas.dorier
4c9db11e6c
Fix broken link 2024-11-25 11:48:11 +09:00
nicolas.dorier
dddd825571
Fix doc of /status 2024-11-25 11:31:25 +09:00
nicolas.dorier
42c9dda280
Rewrite doc of /rescan 2024-11-25 11:17:19 +09:00
nicolas.dorier
9af5962b1e
Improve tags 2024-11-25 11:03:50 +09:00
nicolas.dorier
d5b56d3206
Add /v1 to all paths, reuse parameters definition 2024-11-25 10:41:42 +09:00
Andrew Camilleri
ddde4ae301
Is tracked api (#482) 2024-11-25 10:12:59 +09:00
José Manuel Nieto
a76965d12c
Enrich API documentation (#485)
* Enrich API documentation

* Add missing parameter "includeTransaction" in /cryptos/{cryptoCode}/transactions/{txId}

* Add missing "includeTransaction" parameter in /cryptos/{cryptoCode}/derivations/{derivationScheme}/transactions
2024-11-25 09:31:21 +09:00
Nicolas Dorier
7fb40b5a81
Add browseable doc (#484) 2024-11-18 17:55:58 +09:00
José Manuel Nieto
2e75bdf65e
Add Redoc (#483)
* Add Redoc

* Add CORS Service

* Make sure index.html is served correctly
2024-11-18 17:13:03 +09:00
nicolas.dorier
ac6e63c20f
Bump NBitcoin 2024-11-14 22:11:59 +09:00
nicolas.dorier
5519caf601
Bump NBitcoin 2024-11-14 21:40:51 +09:00
nicolas.dorier
1814138225
Bump NBitcoin 2024-11-13 15:34:05 +09:00
nicolas.dorier
d612420059
bump 2024-11-13 14:47:03 +09:00
nicolas.dorier
f3aeea447d
Fix: Transaction dates are wrong after server / BTC node downtime 2024-11-13 14:46:48 +09:00
nicolas.dorier
261a1e73a1
bump 2024-10-29 10:49:20 +09:00
gruve-p
7c870bdeaa
Bump GRS to 28.0 (#480) 2024-10-29 10:10:30 +09:00
Andrew Camilleri
15a07a8b47
Support mutiny net (#474)
* Support mutiny net

* Small fixup

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-10-29 10:09:52 +09:00
nicolas.dorier
52258aecd1
bump 2024-10-26 10:22:36 +09:00
nicolas.dorier
b672a28c1c
Fix DASH block parsing 2024-10-26 10:21:56 +09:00
nicolas.dorier
a33de3bec7
Make sure index progress has at least one block 2024-10-25 23:57:59 +09:00
nicolas.dorier
f98fa1addb
Fix WatchDog being stuck 2024-09-17 15:46:44 +09:00
nicolas.dorier
b5b90871ae
Remove IRPCClient 2024-09-17 15:39:21 +09:00
nicolas.dorier
e937448b6b
Rewrite for readability 2024-09-17 15:21:20 +09:00
nicolas.dorier
c84c0fce97
Small adaptation for 28.0 2024-08-30 22:32:24 +09:00
nicolas.dorier
4456dd04d0
bump 2024-08-30 16:31:40 +09:00
Nicolas Dorier
6ba0315de4
Merge pull request #475 from NicolasDorier/improve-taproot
Fix taproot PSBT generation
2024-08-30 16:31:21 +09:00
nicolas.dorier
2c6418c058
Fix taproot PSBT generation 2024-08-30 16:15:22 +09:00
Nicolas Dorier
f978e88b85
Merge pull request #469 from NicolasDorier/inputsinfo
Add MatchedInputs to events
2024-06-04 11:52:55 +09:00
nicolas.dorier
22b8e0b17a
Add matchedInputs to events 2024-06-04 11:45:51 +09:00
nicolas.dorier
2bd46f7820
Small refactoring 2024-06-04 09:30:40 +09:00
nicolas.dorier
d03b511db6
Remove useless code 2024-06-03 18:27:02 +09:00
nicolas.dorier
0c9a3569e6
Bump images 2024-05-21 11:03:03 +09:00
Nicolas Dorier
f200ea4930
Merge pull request #466 from NicolasDorier/fixtests
Fix tests
2024-05-21 09:28:00 +09:00
nicolas.dorier
dfef4e8c7e
Fix tests 2024-05-21 00:28:03 +09:00
nicolas.dorier
4bf8e25902
bump packages 2024-05-20 21:23:26 +09:00
Nicolas Dorier
019f8c420a
Merge pull request #463 from NicolasDorier/removedbtrie2
Remove DBTrie
2024-04-08 16:58:16 +09:00
nicolas.dorier
a2dc680338
Remove dead code 2024-04-08 16:43:55 +09:00
nicolas.dorier
72a102a8fc
Split actions into several controllers 2024-04-08 16:31:56 +09:00
nicolas.dorier
e7c701f3f7
Renaming namespace 2024-04-08 15:53:06 +09:00
nicolas.dorier
7c710e34b7
Remove abstractions (Interfaces) 2024-04-08 15:49:53 +09:00
nicolas.dorier
efc9ae6444
Remove abstractions 2024-04-08 14:32:43 +09:00
nicolas.dorier
b20d1567e0
Remove DBTrie 2024-04-08 14:07:00 +09:00
nicolas.dorier
21f5f04f64
Remove debug logs 2024-03-06 19:27:33 +09:00
nicolas.dorier
825d5bc78d
Fix: NBX would sometimes stays stuck in case of reorg (Fix #461 #409) 2024-03-06 16:21:21 +09:00
nicolas.dorier
e83cfdb720
bump npgsql 2024-02-26 09:33:04 +09:00
nicolas.dorier
96633a3b7d
Pass down cancellationToken to RPC commands 2024-02-26 09:11:32 +09:00
nicolas.dorier
900f99545d
Avoid crashing PostgresIndexers.StartAsync 2024-02-05 10:24:39 +09:00
nicolas.dorier
082373501e
bump 2024-01-18 15:43:52 +09:00
nicolas.dorier
279521c507
Bump NBitcoin 2024-01-17 22:07:49 +09:00
nicolas.dorier
2000affe7b
Bump libs 2024-01-17 12:44:56 +09:00
Nicolas Dorier
7d70e72d91
Merge pull request #457 from NicolasDorier/groupapi
Add Group API
2024-01-12 09:10:00 +09:00
nicolas.dorier
9b6358221a
Add Group API 2024-01-10 21:15:04 +09:00
nicolas.dorier
5d4d028b0e
Rename WalletTrackedSource to GroupTrackedSource 2024-01-09 13:09:29 +09:00
nicolas.dorier
07b1193237
bump 2023-12-31 12:32:09 +09:00
nicolas.dorier
56d2293c30
Fix possible nbxplorer sync hanging 2023-12-31 12:31:56 +09:00
nicolas.dorier
84305c84d8
Remove unused code, Use IEnumerable.Chunk instead of custom Batch method 2023-12-30 23:45:30 +09:00
nicolas.dorier
fffa6f0bca
bump 2023-12-25 14:20:38 +09:00
Nicolas Dorier
15eee7ae47
Merge pull request #456 from NicolasDorier/foiqntq
Remove useless db roundtrip when indexing transactions
2023-12-25 14:20:21 +09:00
nicolas.dorier
a490c24351
Remove useless db roundtrip when indexing transactions 2023-12-25 14:15:24 +09:00
nicolas.dorier
6eb53ed8d2
Update sln 2023-12-18 13:33:15 +09:00
Nicolas Dorier
6ca2560a1a
Merge pull request #454 from gruve-p/patch-4
Bump Groestlcoin Core to 26.0
2023-12-18 13:31:41 +09:00
Nicolas Dorier
f69581f69e
Merge pull request #455 from dgarage/rewriteci
Use buildx
2023-12-18 13:31:25 +09:00
nicolas.dorier
190eabbc5e
Use buildx 2023-12-18 13:30:26 +09:00
gruve-p
826dc2d4ac
Bump Groestlcoin Core to 26.0 2023-12-13 17:45:39 +01:00
nicolas.dorier
95f28ac578
Properly delete chain-slim.dat 2023-12-12 19:11:05 +09:00
nicolas.dorier
8d309e4de5
bump NBX 2023-12-12 19:06:14 +09:00
nicolas.dorier
9bcf963a4a
Fix weird error during migration 2023-12-12 18:59:00 +09:00
nicolas.dorier
85f508756a
Bump deps 2023-12-12 13:05:16 +09:00
nicolas.dorier
5ddba1d816
bump 2023-12-11 16:17:56 +09:00
Nicolas Dorier
eca963def6
Merge pull request #453 from NicolasDorier/txts3
Add WalletTrackedSource to backend
2023-12-11 16:02:43 +09:00
nicolas.dorier
cfedff079d
Add WalletTrackedSource to backend 2023-12-11 15:57:23 +09:00
Nicolas Dorier
ceef9651f3
Merge pull request #452 from dgarage/watchdog
Fix spurious NBXplorer stuck synching
2023-12-11 13:23:10 +09:00
nicolas.dorier
1d89e331ed
Fix spurious NBXplorer stuck synching 2023-12-11 10:39:42 +09:00
Nicolas Dorier
7860ebbda4
Merge pull request #451 from NicolasDorier/refactorgetblocks
Refactor getting block headers from RPC efficiently
2023-12-06 17:49:53 +09:00
nicolas.dorier
2f855b5cc3
Refactor getting block headers from RPC efficiently 2023-12-06 17:43:39 +09:00
Nicolas Dorier
8ec608f2bb
Merge pull request #449 from NicolasDorier/trackctx
Introduce TrackedSourceContext
2023-12-05 13:06:05 +09:00
Kukks
4260cb8e04
Introduce TrackedSourceContext 2023-12-05 12:51:49 +09:00
nicolas.dorier
9b6c0010fb
Fix spurious deadlocks 2023-12-04 08:53:20 +09:00
nicolas.dorier
a01893d09d
Remove chain-slim.dat after migration to postgres 2023-11-30 08:14:26 +09:00
nicolas.dorier
57048b0d05
bump 2023-11-27 12:10:35 +09:00
Nicolas Dorier
9a64c246b8
Merge pull request #448 from NicolasDorier/qoinq
Replaced[] wasn't set for transactions mined into a block
2023-11-27 08:53:09 +09:00
nicolas.dorier
fa26cd4fcf
Replaced[] wasn't set for transactions mined into a block 2023-11-24 18:16:20 +09:00
nicolas.dorier
3b192971f0
fixup 2023-11-24 16:41:18 +09:00
Nicolas Dorier
02e4714bce
Merge pull request #447 from NicolasDorier/fwpoeqz
Fix double spend detection (Fix #421)
2023-11-24 09:44:06 +09:00
nicolas.dorier
342a16ad07
Fix double spend detection (Fix #421) 2023-11-24 09:29:25 +09:00
nicolas.dorier
31a1ab4c89
bump NBX 2023-11-21 12:49:00 +09:00
nicolas.dorier
5de2a002a0
bump npgsql 2023-11-21 11:31:01 +09:00
Nicolas Dorier
9faab8f97d
Merge pull request #446 from NicolasDorier/fixuri
Make sure NBXClient's uris are well escaped
2023-11-17 15:41:51 +09:00
nicolas.dorier
dbef90b927
Do not use connection pooling for the refresh wallet history 2023-11-17 15:32:43 +09:00
nicolas.dorier
30c1536323
Make sure NBXClient's uris are well encoded 2023-11-17 15:29:48 +09:00
nicolas.dorier
ef1f78b9e9
Improve performance of get_wallet_recents 2023-11-16 10:09:15 +09:00
nicolas.dorier
d7a9af1d7a
Do not hardcode public schema in migration scripts 2023-11-16 07:42:15 +09:00
Nicolas Dorier
1ac249db79
Merge pull request #442 from NicolasDorier/bumplibs
Bump dependencies
2023-11-15 21:56:48 +09:00
nicolas.dorier
c547f20d20
Bump dependencies 2023-11-15 20:00:18 +09:00
nicolas.dorier
a9a8f56d73
Bump commandtimeout for migration 2023-11-15 18:13:25 +09:00
nicolas.dorier
42dbfdb59d
bump client lib 2023-11-15 15:42:29 +09:00
nicolas.dorier
f8707aca8b
Update README.md 2023-11-15 13:08:42 +09:00
nicolas.dorier
f76543311d
Update README 2023-11-15 13:07:31 +09:00
Nicolas Dorier
8a051016f4
Merge pull request #441 from NicolasDorier/net8
Bump to .NET 8.0
2023-11-15 13:05:13 +09:00
nicolas.dorier
506388937e
Bump to .NET 8.0 2023-11-15 12:57:47 +09:00
Nicolas Dorier
600a642ebc
Merge pull request #440 from dgarage/qointee
Stop supporting DBTrie backend
2023-11-15 11:33:01 +09:00
nicolas.dorier
3cbb864713
Stop supporting DBTrie backend 2023-11-15 11:22:30 +09:00
Nicolas Dorier
25a3f579d8
Merge pull request #437 from dgarage/wionqr
Allow creation of PSBTs conflicting with mempool with includeOnlyOutpoints
2023-11-15 11:14:55 +09:00
nicolas.dorier
a858d56552
Allow creation of PSBTs conflicting with mempool with includeOnlyOutpoints 2023-11-15 11:05:24 +09:00
Nicolas Dorier
06ad54141e
Merge pull request #377 from Kukks/add-input-index-to-output
Add InputIndex to matched inputs
2023-11-15 11:03:25 +09:00
Kukks
ef84e74bbb
Add InputIndex to matched inputs 2023-11-15 10:40:36 +09:00
nicolas.dorier
3579fcd226
Fix: Confirmation status of unrelated double spend txs was not updated 2023-11-15 09:29:46 +09:00
172 changed files with 11445 additions and 6399 deletions

View File

@ -10,61 +10,19 @@ jobs:
cd .circleci && ./run-tests.sh
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
amd64:
machine:
enabled: true
steps:
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -t $DOCKERHUB_REPO:latest-amd64 -f Dockerfile.linuxamd64 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
arm32v7:
machine:
enabled: true
docker:
docker:
- image: cimg/base:stable
steps:
- checkout
- setup_remote_docker
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
arm64v8:
machine:
enabled: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f Dockerfile.linuxarm64v8 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
multiarch:
machine:
enabled: true
steps:
- run:
command: |
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
docker buildx create --use
docker buildx build -t $DOCKERHUB_REPO:$LATEST_TAG --platform linux/amd64,linux/arm64,linux/arm/v7 --push .
workflows:
version: 2
@ -74,33 +32,12 @@ workflows:
publish:
jobs:
- amd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- arm32v7:
- docker:
filters:
branches:
ignore: /.*/
# only act on version tags v1.0.0.88 or v1.0.2-1
# OR feature tags like abc
# OR features on specific versions like v1.0.0.88-abc-1
tags:
only: /v[1-9]+(\.[0-9]+)*/
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/

View File

@ -1,5 +1,4 @@
# Note that we are using buster rather than bulleyes. Somehow, raspberry pi 4 doesn't like bulleyes.
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
WORKDIR /source
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
@ -9,10 +8,12 @@ COPY . .
RUN cd NBXplorer && \
dotnet publish --output /app/ --configuration Release
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim-arm32v7
WORKDIR /datadir
FROM mcr.microsoft.com/dotnet/aspnet:10.0.9-noble
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
RUN mkdir /datadir
ENV NBXPLORER_DATADIR=/datadir
VOLUME /datadir

View File

@ -1,19 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
WORKDIR /source
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
# Cache some dependencies
RUN cd NBXplorer && dotnet restore && cd ..
COPY . .
RUN cd NBXplorer && \
dotnet publish --output /app/ --configuration Release
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim
WORKDIR /app
RUN mkdir /datadir
ENV NBXPLORER_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "NBXplorer.dll"]

View File

@ -1,20 +0,0 @@
# This is a manifest image, will pull the image with the same arch as the builder machine
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
WORKDIR /source
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
# Cache some dependencies
RUN cd NBXplorer && dotnet restore && cd ..
COPY . .
RUN cd NBXplorer && \
dotnet publish --output /app/ --configuration Release
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim-arm64v8
WORKDIR /datadir
WORKDIR /app
ENV NBXPLORER_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "NBXplorer.dll"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,20 +1,49 @@
using NBitcoin;
#nullable enable
using NBitcoin;
using System.Collections.Generic;
#if !NO_RECORD
using static NBitcoin.WalletPolicies.MiniscriptNode;
#endif
namespace NBXplorer.DerivationStrategy
{
public class Derivation
{
public Derivation()
public Derivation(Script scriptPubKey, Script? redeem = null)
{
ScriptPubKey = scriptPubKey;
Redeem = redeem;
}
public Script ScriptPubKey
{
get; set;
get;
}
public Script Redeem
public Script? Redeem
{
get; set;
}
}
public class KeyPathDerivation : Derivation
{
public KeyPathDerivation(KeyPath keyPath, Script scriptPubKey, Script? redeem = null)
: base(scriptPubKey, redeem)
{
KeyPath = keyPath;
}
public KeyPath KeyPath { get; }
}
#if !NO_RECORD
public class PolicyDerivation : Derivation
{
public PolicyDerivation(NBitcoin.WalletPolicies.DerivationResult details, Script scriptPubKey, Script? redeem = null)
: base(scriptPubKey, redeem)
{
Details = details;
}
public NBitcoin.WalletPolicies.DerivationResult Details { get; }
}
#endif
}

View File

@ -1,8 +1,12 @@
using NBitcoin;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
namespace NBXplorer.DerivationStrategy
@ -52,7 +56,6 @@ namespace NBXplorer.DerivationStrategy
public HashSet<string> AuthorizedOptions { get; } = new HashSet<string>();
readonly Regex MultiSigRegex = new Regex("^([0-9]{1,2})-of(-[A-Za-z0-9]+)+$");
static DirectDerivationStrategy DummyPubKey = new DirectDerivationStrategy(new ExtKey().Neuter().GetWif(Network.RegTest), false);
public DerivationStrategyBase Parse(string str)
{
var strategy = ParseCore(str);
@ -79,6 +82,8 @@ namespace NBXplorer.DerivationStrategy
if (!Extensions.TryAdd(optionsDictionary, key, value))
throw new FormatException($"The option '{key}' is duplicated");
}
var hasOptions = optionsDictionary.Count != 0;
str = _OptionRegex.Replace(str, string.Empty);
if (optionsDictionary.Remove("legacy"))
{
@ -142,6 +147,14 @@ namespace NBXplorer.DerivationStrategy
.ToArray();
return CreateMultiSigDerivationStrategy(pubKeys, sigCount, options);
}
#if !NO_RECORD
else if (PolicyDerivationStrategy._MaybeMiniscript.IsMatch(str))
{
if (hasOptions)
throw new FormatException("The derivation scheme should not contain any option (such as -[legacy])");
return PolicyDerivationStrategy.Parse(str, _Network);
}
#endif
else
{
var key = _Network.Parse<BitcoinExtPubKey>(str);
@ -155,7 +168,7 @@ namespace NBXplorer.DerivationStrategy
/// <param name="publicKey">The public key of the wallet</param>
/// <param name="options">Derivation options</param>
/// <returns></returns>
public DerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
{
return CreateDirectDerivationStrategy(publicKey.GetWif(Network), options);
}
@ -166,10 +179,10 @@ namespace NBXplorer.DerivationStrategy
/// <param name="publicKey">The public key of the wallet</param>
/// <param name="options">Derivation options</param>
/// <returns></returns>
public DerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
{
options = options ?? new DerivationStrategyOptions();
DerivationStrategyBase strategy = null;
StandardDerivationStrategyBase strategy = null;
#pragma warning disable CS0618 // Type or member is obsolete
if (options.ScriptPubKeyType != ScriptPubKeyType.TaprootBIP86)
#pragma warning restore CS0618 // Type or member is obsolete
@ -224,7 +237,7 @@ namespace NBXplorer.DerivationStrategy
public DerivationStrategyBase CreateMultiSigDerivationStrategy(BitcoinExtPubKey[] pubKeys, int sigCount, DerivationStrategyOptions options = null)
{
options = options ?? new DerivationStrategyOptions();
DerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
StandardDerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
if (options.ScriptPubKeyType == ScriptPubKeyType.Legacy)
return new P2SHDerivationStrategy(derivationStrategy, false);
@ -237,19 +250,6 @@ namespace NBXplorer.DerivationStrategy
}
return derivationStrategy;
}
private void ReadBool(ref string str, string attribute, ref bool value)
{
value = str.Contains($"[{attribute}]");
if (value)
{
str = str.Replace($"[{attribute}]", string.Empty);
str = str.Replace("--", "-");
if (str.EndsWith("-"))
str = str.Substring(0, str.Length - 1);
}
}
readonly static Regex _OptionRegex = new Regex(@"-\[([^ \]\-]+)\]");
}
}

View File

@ -6,7 +6,7 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class DirectDerivationStrategy : DerivationStrategyBase
public class DirectDerivationStrategy : StandardDerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -44,15 +44,11 @@ namespace NBXplorer.DerivationStrategy
_Root = root;
Segwit = segwit;
}
public override Derivation GetDerivation()
{
var pubKey = _Root.ExtPubKey.PubKey;
return new Derivation() { ScriptPubKey = Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey };
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
public override Derivation GetDerivation(KeyPath keyPath)
{
return new DirectDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network), Segwit, AdditionalOptions);
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey;
return new KeyPathDerivation(keyPath, Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey);
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()

View File

@ -1,4 +1,8 @@
using NBitcoin;
#nullable enable
using NBitcoin;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -13,40 +17,42 @@ namespace NBXplorer.DerivationStrategy
Direct = 2,
Custom = 3,
}
public abstract class DerivationStrategyBase : IHDScriptPubKey
public abstract class StandardDerivationStrategyBase : DerivationStrategyBase, IHDScriptPubKey
{
ReadOnlyDictionary<string, string> Empty = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0));
internal StandardDerivationStrategyBase(ReadOnlyDictionary<string, string> additionalOptions) : base(additionalOptions)
{
}
public abstract Derivation GetDerivation(KeyPath keyPath);
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature)
=> new KeyPathTemplateDerivationLine(this, keyPathTemplates, feature);
Script IHDScriptPubKey.ScriptPubKey => GetDerivation(KeyPath.Empty).ScriptPubKey;
IHDScriptPubKey? IHDScriptPubKey.Derive(KeyPath keyPath) => keyPath.IsHardenedPath ? null : new HDScriptPubKey(this, keyPath);
class HDScriptPubKey(StandardDerivationStrategyBase Parent, KeyPath KeyPath) : IHDScriptPubKey
{
public Script ScriptPubKey => Parent.GetDerivation(KeyPath).ScriptPubKey;
public IHDScriptPubKey? Derive(KeyPath keyPath) => KeyPath.IsHardenedPath ? null : new HDScriptPubKey(Parent, KeyPath.Derive(keyPath));
}
}
public abstract class DerivationStrategyBase
{
readonly ReadOnlyDictionary<string, string> Empty = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0));
public ReadOnlyDictionary<string, string> AdditionalOptions { get; }
internal DerivationStrategyBase(ReadOnlyDictionary<string,string> additionalOptions)
internal DerivationStrategyBase(ReadOnlyDictionary<string,string>? additionalOptions)
{
AdditionalOptions = additionalOptions ?? Empty;
}
public DerivationLine GetLineFor(KeyPathTemplate keyPathTemplate)
{
return new DerivationLine(this, keyPathTemplate);
}
public abstract DerivationStrategyBase GetChild(KeyPath keyPath);
public Derivation GetDerivation(uint i)
{
return GetChild(new KeyPath(i)).GetDerivation();
}
public Derivation GetDerivation(KeyPath keyPath)
{
if (keyPath == null || keyPath.Length == 0)
return GetDerivation();
return GetChild(keyPath).GetDerivation();
}
public abstract Derivation GetDerivation();
public DerivationLine GetLineFor(DerivationFeature feature) => GetLineFor(KeyPathTemplates.Default, feature);
public abstract DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature);
protected internal abstract string StringValueCore
{
get;
}
string _StringValue;
string? _StringValue;
string StringValue
{
get
@ -66,96 +72,74 @@ namespace NBXplorer.DerivationStrategy
{
return string.Join("", new SortedDictionary<string, string>(AdditionalOptions).Select(pair => $"-[{pair.Key}{(string.IsNullOrEmpty(pair.Value)?string.Empty: $"={pair.Value}")}]"));
}
public override bool Equals(object obj)
{
DerivationStrategyBase item = obj as DerivationStrategyBase;
if(item == null)
return false;
return StringValue.Equals(item.StringValue);
}
public static bool operator ==(DerivationStrategyBase a, DerivationStrategyBase b)
{
if(System.Object.ReferenceEquals(a, b))
return true;
if(((object)a == null) || ((object)b == null))
return false;
return a.StringValue == b.StringValue;
}
public static bool operator !=(DerivationStrategyBase a, DerivationStrategyBase b)
{
return !(a == b);
}
#nullable enable
public override bool Equals(object? obj) => obj is DerivationStrategyBase o && StringValue.Equals(o.StringValue);
public static bool operator ==(DerivationStrategyBase? a, DerivationStrategyBase? b) => a is null ? b is null : a.Equals(b);
public static bool operator !=(DerivationStrategyBase? a, DerivationStrategyBase? b) => !(a == b);
public override int GetHashCode() => StringValue.GetHashCode();
#nullable restore
public abstract IEnumerable<ExtPubKey> GetExtPubKeys();
public override int GetHashCode()
{
return StringValue.GetHashCode();
}
public override string ToString()
{
return StringValue;
}
Script IHDScriptPubKey.ScriptPubKey => GetDerivation().ScriptPubKey;
IHDScriptPubKey IHDScriptPubKey.Derive(KeyPath keyPath)
{
return GetChild(keyPath);
}
class HDRedeemScriptPubKey : IHDScriptPubKey
{
private readonly DerivationStrategyBase strategyBase;
public HDRedeemScriptPubKey(DerivationStrategyBase strategyBase)
{
this.strategyBase = strategyBase;
}
public Script ScriptPubKey => strategyBase.GetDerivation().Redeem;
public bool CanDeriveHardenedPath()
{
return strategyBase.CanDeriveHardenedPath();
}
public IHDScriptPubKey Derive(KeyPath keyPath)
{
return strategyBase.GetChild(keyPath).AsHDRedeemScriptPubKey();
}
}
public IHDScriptPubKey AsHDRedeemScriptPubKey()
{
return new HDRedeemScriptPubKey(this);
}
public bool CanDeriveHardenedPath()
{
return false;
}
}
public class DerivationLine
#if !NO_RECORD
public class MiniscriptDerivationLine : DerivationLine
{
public DerivationLine(DerivationStrategyBase derivationStrategyBase, KeyPathTemplate keyPathTemplate)
public MiniscriptDerivationLine(PolicyDerivationStrategy derivationStrategy, DerivationFeature derivationFeature) : base(derivationFeature)
{
DerivationStrategy = derivationStrategy;
Intent = ToAddressIntent(derivationFeature);
}
public static AddressIntent ToAddressIntent(DerivationFeature derivationFeature)
{
return derivationFeature switch
{
DerivationFeature.Change => AddressIntent.Change,
DerivationFeature.Deposit => AddressIntent.Deposit,
_ => throw new NotSupportedException("MiniscriptDerivationStrategy only support deposit and change features")
};
}
public PolicyDerivationStrategy DerivationStrategy { get; }
public AddressIntent Intent { get; }
public override Derivation Derive(uint index) => DerivationStrategy.GetDerivation(Intent, index);
}
#endif
public abstract class DerivationLine
{
protected DerivationLine(DerivationFeature feature)
{
Feature = feature;
}
public DerivationFeature Feature { get; }
public abstract Derivation Derive(uint index);
}
public class KeyPathTemplateDerivationLine : DerivationLine
{
public KeyPathTemplateDerivationLine(StandardDerivationStrategyBase derivationStrategyBase, KeyPathTemplates keyPathTemplates, DerivationFeature derivationFeature) : base(derivationFeature)
{
if (derivationStrategyBase == null)
throw new ArgumentNullException(nameof(derivationStrategyBase));
if (keyPathTemplate == null)
throw new ArgumentNullException(nameof(keyPathTemplate));
if (keyPathTemplates == null)
throw new ArgumentNullException(nameof(keyPathTemplates));
DerivationStrategyBase = derivationStrategyBase;
KeyPathTemplate = keyPathTemplate;
KeyPathTemplate = keyPathTemplates.GetKeyPathTemplate(derivationFeature);
}
public DerivationStrategyBase DerivationStrategyBase { get; }
public StandardDerivationStrategyBase DerivationStrategyBase { get; }
public KeyPathTemplate KeyPathTemplate { get; }
DerivationStrategyBase _PreLine;
public Derivation Derive(uint index)
public override Derivation Derive(uint index)
{
_PreLine = _PreLine ?? DerivationStrategyBase.GetChild(KeyPathTemplate.PreIndexes);
return _PreLine.GetDerivation(new KeyPath(index).Derive(KeyPathTemplate.PostIndexes));
var kp = KeyPathTemplate.GetKeyPath(index);
return DerivationStrategyBase.GetDerivation(kp);
}
}
}

View File

@ -9,7 +9,7 @@ using System.Collections.ObjectModel;
namespace NBXplorer.DerivationStrategy
{
public class MultisigDerivationStrategy : DerivationStrategyBase
public class MultisigDerivationStrategy : StandardDerivationStrategyBase
{
public bool LexicographicOrder
{
@ -62,29 +62,19 @@ namespace NBXplorer.DerivationStrategy
get;
}
private void WriteBytes(MemoryStream ms, byte[] v)
{
ms.Write(v, 0, v.Length);
}
public override Derivation GetDerivation()
public override Derivation GetDerivation(KeyPath keyPath)
{
var pubKeys = new PubKey[this.Keys.Count];
Parallel.For(0, pubKeys.Length, i =>
{
pubKeys[i] = this.Keys[i].ExtPubKey.PubKey;
pubKeys[i] = this.Keys[i].ExtPubKey.Derive(keyPath).PubKey;
});
if(LexicographicOrder)
{
Array.Sort(pubKeys, LexicographicComparer);
}
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(RequiredSignatures, pubKeys);
return new Derivation() { ScriptPubKey = redeem };
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
{
return new MultisigDerivationStrategy(RequiredSignatures, Keys.Select(k => k.ExtPubKey.Derive(keyPath).GetWif(k.Network)).ToArray(), IsLegacy, LexicographicOrder, AdditionalOptions);
return new KeyPathDerivation(keyPath, redeem);
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()

View File

@ -4,10 +4,10 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class P2SHDerivationStrategy : DerivationStrategyBase
public class P2SHDerivationStrategy : StandardDerivationStrategyBase
{
bool addSuffix;
internal P2SHDerivationStrategy(DerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
internal P2SHDerivationStrategy(StandardDerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
{
if(inner == null)
throw new ArgumentNullException(nameof(inner));
@ -15,7 +15,7 @@ namespace NBXplorer.DerivationStrategy
this.addSuffix = addSuffix;
}
public DerivationStrategyBase Inner
public StandardDerivationStrategyBase Inner
{
get; set;
}
@ -30,24 +30,18 @@ namespace NBXplorer.DerivationStrategy
}
}
public override Derivation GetDerivation()
{
var derivation = Inner.GetDerivation();
return new Derivation()
{
ScriptPubKey = derivation.ScriptPubKey.Hash.ScriptPubKey,
Redeem = derivation.Redeem ?? derivation.ScriptPubKey
};
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()
{
return Inner.GetExtPubKeys();
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
public override Derivation GetDerivation(KeyPath keyPath)
{
return new P2SHDerivationStrategy(Inner.GetChild(keyPath), addSuffix);
var derivation = Inner.GetDerivation(keyPath);
return new KeyPathDerivation(
keyPath,
derivation.ScriptPubKey.Hash.ScriptPubKey,
derivation.Redeem ?? derivation.ScriptPubKey);
}
}
}

View File

@ -4,40 +4,31 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class P2WSHDerivationStrategy : DerivationStrategyBase
public class P2WSHDerivationStrategy : StandardDerivationStrategyBase
{
internal P2WSHDerivationStrategy(DerivationStrategyBase inner):base(inner.AdditionalOptions)
internal P2WSHDerivationStrategy(StandardDerivationStrategyBase inner):base(inner.AdditionalOptions)
{
if(inner == null)
throw new ArgumentNullException(nameof(inner));
Inner = inner;
}
public DerivationStrategyBase Inner
public StandardDerivationStrategyBase Inner
{
get; set;
}
protected internal override string StringValueCore => Inner.ToString();
public override Derivation GetDerivation()
{
var derivation = Inner.GetDerivation();
return new Derivation()
{
ScriptPubKey = derivation.ScriptPubKey.WitHash.ScriptPubKey,
Redeem = derivation.ScriptPubKey
};
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()
{
return Inner.GetExtPubKeys();
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
public override Derivation GetDerivation(KeyPath keyPath)
{
return new P2WSHDerivationStrategy(Inner.GetChild(keyPath));
var redeem = Inner.GetDerivation(keyPath).ScriptPubKey;
return new KeyPathDerivation(keyPath, redeem.WitHash.ScriptPubKey, redeem);
}
}
}

View File

@ -0,0 +1,212 @@
#nullable enable
#if !NO_RECORD
using NBitcoin;
using NBitcoin.WalletPolicies;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
namespace NBXplorer.DerivationStrategy
{
public class PolicyDerivationStrategy : DerivationStrategyBase
{
internal static readonly Regex _MaybeMiniscript = new("^(wsh|sh|pkh|tr|wpkh)\\(");
public static bool TryParse(
string str,
Network network,
[MaybeNullWhen(false)] out PolicyDerivationStrategy strategy)
{
strategy = null;
if (!_MaybeMiniscript.IsMatch(str))
return false;
if (!WalletPolicy.TryParse(str, network, out var policy) || !IsValidPolicy(policy, out _))
return false;
strategy = new PolicyDerivationStrategy(policy, false);
return true;
}
public static PolicyDerivationStrategy Parse(string str, Network network)
{
if (!_MaybeMiniscript.IsMatch(str))
throw new FormatException("The policy should start by either wsh, sh, pkh, tr, wpkh");
var policy = WalletPolicy.Parse(str, network);
if (!IsValidPolicy(policy, out var err))
throw new FormatException(err);
return new PolicyDerivationStrategy(policy, false);
}
public PolicyDerivationStrategy(WalletPolicy policy) : this(policy, true)
{
}
PolicyDerivationStrategy(WalletPolicy policy, bool check) : base(null)
{
if (check && !IsValidPolicy(policy, out var error))
throw new ArgumentException(paramName: nameof(policy), message: error);
Policy = policy;
}
/// <summary>
/// Check that the policy should have at least one multi path node ([12345678]xpub/**) and no xpriv
/// </summary>
/// <param name="policy"></param>
/// <param name="error"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public static bool IsValidPolicy(WalletPolicy policy, [MaybeNullWhen(true)] out string error)
{
var v = new ValidPolicyVisitor();
policy.FullDescriptor.Visit(v);
error = v.Error;
return error is null;
}
class ValidPolicyVisitor : MiniscriptVisitor
{
public string? Error {
get
{
if (hasSecretKey)
return "The policy should not contain any xpriv key";
if (!hasMultiPathNode)
return "The policy should contain at least one multi path node ([12345678]xpub/**)";
return null;
}
}
private bool hasMultiPathNode;
private bool hasSecretKey;
public override void Visit(MiniscriptNode node)
{
if (node is MiniscriptNode.MultipathNode)
hasMultiPathNode = true;
else if (node is MiniscriptNode.HDKeyNode { Key: BitcoinExtKey })
hasSecretKey = true;
else
base.Visit(node);
}
}
public WalletPolicy Policy { get; }
private readonly DerivationCache cache = new();
private string? _str;
protected internal override string StringValueCore => _str ??= Policy.ToString(true);
public override IEnumerable<ExtPubKey> GetExtPubKeys()
=> Policy.KeyInformationVector.Select(kv => GetExtPubKey(kv.Key));
private ExtPubKey GetExtPubKey(IHDKey key)
=> key switch
{
ExtPubKey extPubKey => extPubKey,
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
ExtKey extKey => extKey.Neuter(),
BitcoinExtKey bitcoinExtKey => bitcoinExtKey.ExtKey.Neuter(),
_ => throw new NotSupportedException($"Unsupported key type: {key.GetType()}")
};
public NBXplorer.DerivationStrategy.Derivation GetDerivation(DerivationFeature feature, uint index)
=> GetDerivation(MiniscriptDerivationLine.ToAddressIntent(feature), index);
public NBXplorer.DerivationStrategy.Derivation GetDerivation(AddressIntent addressIntent, uint index)
{
var derived = Policy.FullDescriptor.Derive(new(addressIntent, [(int)index]) { DervivationCache = cache });
var scripts = derived[0].Miniscript.ToScripts();
return new PolicyDerivation(derived[0], scripts.ScriptPubKey, scripts.RedeemScript);
}
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature) => new MiniscriptDerivationLine(this, feature);
// Extract the multipath node from the hdkey
class MultipathNodeVisitor : MiniscriptVisitor
{
private readonly ExtPubKey _target;
public MiniscriptNode.MultipathNode? Result { get; set; }
public MultipathNodeVisitor(IHDKey target)
{
ArgumentNullException.ThrowIfNull(target);
_target = Normalize(target);
}
private static ExtPubKey Normalize(IHDKey target)
=> target switch
{
BitcoinExtKey extKey => extKey.Neuter().ExtPubKey,
ExtKey extKey => extKey.Neuter(),
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
ExtPubKey a => a,
_ => throw new NotSupportedException(target.GetType().ToString())
};
public override void Visit(MiniscriptNode node)
{
if (Result is not null)
return;
if (node is MiniscriptNode.MultipathNode { Target: MiniscriptNode.HDKeyNode hd } mp)
{
if (Normalize(hd.Key).Equals(_target))
Result = mp;
}
else
base.Visit(node);
}
}
class MiniscriptScriptPubKey : IHDScriptPubKey
{
private readonly PolicyDerivationStrategy _policyDerivationStrategy;
private readonly MiniscriptNode.MultipathNode _multipathNode;
private readonly KeyPath _keyPath;
public MiniscriptScriptPubKey(
PolicyDerivationStrategy policyDerivationStrategy,
MiniscriptNode.MultipathNode multipathNode,
KeyPath? keyPath = null,
DerivationCache? cache = null)
{
_policyDerivationStrategy = policyDerivationStrategy;
_multipathNode = multipathNode;
_keyPath = keyPath ?? KeyPath.Empty;
_cache = cache ?? new();
}
private readonly DerivationCache _cache;
public IHDScriptPubKey? Derive(KeyPath keyPath) =>
_keyPath.Derive(keyPath) is { Length: <= 2 } kp
&& (kp.Length == 0 || GetAddressIntent(kp.Indexes[0]) is not null)
&& !kp.IsHardenedPath
? new MiniscriptScriptPubKey(_policyDerivationStrategy, _multipathNode, kp, _cache) : null;
public Script ScriptPubKey
{
get
{
if (_keyPath is not { Indexes: [var intentIdx, var index], IsHardenedPath: false }
|| GetAddressIntent(intentIdx) is not {} intent)
throw new InvalidOperationException("Invalid keypath (it should be non hardened with two component)");
var derived = _policyDerivationStrategy.Policy.FullDescriptor.Derive(new(intent, new[] { (int)index })
{
DervivationCache = _cache
});
return derived[0].Miniscript.ToScripts().ScriptPubKey;
}
}
private AddressIntent? GetAddressIntent(uint intentIdx)
=> intentIdx == _multipathNode.DepositIndex ? AddressIntent.Deposit :
intentIdx == _multipathNode.ChangeIndex ? AddressIntent.Change : null;
}
public IHDScriptPubKey? GetHDScriptPubKey(IHDKey accountKey)
{
ArgumentNullException.ThrowIfNull(accountKey);
var visitor = new MultipathNodeVisitor(accountKey);
visitor.Visit(Policy.FullDescriptor.RootNode);
if (visitor.Result is null)
return null;
return new MiniscriptScriptPubKey(this, visitor.Result);
}
}
}
#endif

View File

@ -6,7 +6,7 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class TaprootDerivationStrategy : DerivationStrategyBase
public class TaprootDerivationStrategy : StandardDerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -35,21 +35,16 @@ namespace NBXplorer.DerivationStrategy
throw new ArgumentNullException(nameof(root));
_Root = root;
}
public override Derivation GetDerivation()
public override Derivation GetDerivation(KeyPath keyPath)
{
#if NO_SPAN
throw new NotSupportedException("Deriving taproot address is not supported on this platform.");
#else
var pubKey = _Root.ExtPubKey.PubKey.GetTaprootFullPubKey();
return new Derivation() { ScriptPubKey = pubKey.ScriptPubKey };
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey.GetTaprootFullPubKey();
return new KeyPathDerivation(keyPath, pubKey.ScriptPubKey);
#endif
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
{
return new TaprootDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network), AdditionalOptions);
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()
{
yield return _Root.ExtPubKey;

View File

@ -13,6 +13,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
using NBitcoin.RPC;
using System.Runtime.CompilerServices;
using System.Linq;
namespace NBXplorer
{
@ -150,7 +152,7 @@ namespace NBXplorer
}
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default)
{
return await SendAsync<TransactionResult>(HttpMethod.Get, null, "v1/cryptos/{0}/transactions/" + txId, new[] { CryptoCode }, cancellation).ConfigureAwait(false);
return await SendAsync<TransactionResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/transactions/{txId}", cancellation).ConfigureAwait(false);
}
public TransactionResult GetTransaction(uint256 txId, CancellationToken cancellation = default)
@ -162,7 +164,7 @@ namespace NBXplorer
{
if (extKey == null)
throw new ArgumentNullException(nameof(extKey));
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, "v1/cryptos/{0}/derivations/{1}/prune", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/prune", cancellation).ConfigureAwait(false);
}
public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequest, CancellationToken cancellation = default)
@ -170,6 +172,16 @@ namespace NBXplorer
return PruneAsync(extKey, pruneRequest, cancellation).GetAwaiter().GetResult();
}
internal class RawStr
{
private string str;
public RawStr(string str)
{
this.str = str;
}
public override string ToString() => str;
};
internal static RawStr Raw(string str) => new RawStr(str);
public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
{
if (extKey == null)
@ -184,7 +196,7 @@ namespace NBXplorer
var argsString = string.Join("&", args.ToArray());
if (argsString != string.Empty)
argsString = $"?{argsString}";
await SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan{2}", new object[] { Network.CryptoCode, extKey, argsString }, cancellation).ConfigureAwait(false);
await SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);
}
public void ScanUTXOSet(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
{
@ -193,7 +205,7 @@ namespace NBXplorer
public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
{
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan", cancellation).ConfigureAwait(false);
}
public ScanUTXOInformation GetScanUTXOSetInformation(DerivationStrategyBase extKey, CancellationToken cancellation = default)
@ -217,25 +229,28 @@ namespace NBXplorer
await session.ConnectAsync(cancellation).ConfigureAwait(false);
return session;
}
public WebsocketNotificationSessionLegacy CreateWebsocketNotificationSessionLegacy(CancellationToken cancellation = default)
{
return CreateWebsocketNotificationSessionLegacyAsync(cancellation).GetAwaiter().GetResult();
}
public async Task<WebsocketNotificationSessionLegacy> CreateWebsocketNotificationSessionLegacyAsync(CancellationToken cancellation = default)
{
var session = new WebsocketNotificationSessionLegacy(this);
await session.ConnectAsync(cancellation).ConfigureAwait(false);
return session;
}
public UTXOChanges GetUTXOs(TrackedSource trackedSource, CancellationToken cancellation = default)
{
return GetUTXOsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
}
public async Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
public Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
return await SendAsync<UTXOChanges>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos", new object[] { CryptoCode, dsts.DerivationStrategy.ToString() }, cancellation).ConfigureAwait(false);
}
else if (trackedSource is AddressTrackedSource asts)
{
return await SendAsync<UTXOChanges>(HttpMethod.Get, null, "v1/cryptos/{0}/addresses/{1}/utxos", new object[] { CryptoCode, asts.Address }, cancellation).ConfigureAwait(false);
}
else
throw UnSupported(trackedSource);
return SendAsync<UTXOChanges>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/utxos", cancellation);
}
public void WaitServerStarted(CancellationToken cancellation = default)
@ -266,7 +281,7 @@ namespace NBXplorer
}
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
{
return TrackAsync(TrackedSource.Create(strategy), cancellation);
return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation);
}
public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
@ -277,27 +292,18 @@ namespace NBXplorer
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, strategy.ToString() }, cancellation).ConfigureAwait(false);
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, $"v1/cryptos/{CryptoCode}/derivations/{strategy}", cancellation).ConfigureAwait(false);
}
public void Track(TrackedSource trackedSource, CancellationToken cancellation = default)
{
TrackAsync(trackedSource, cancellation).GetAwaiter().GetResult();
TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult();
}
public Task TrackAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
return SendAsync<string>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, dsts.DerivationStrategy.ToString() }, cancellation);
}
else if (trackedSource is AddressTrackedSource asts)
{
return SendAsync<string>(HttpMethod.Post, null, "v1/cryptos/{0}/addresses/{1}", new[] { CryptoCode, asts.Address.ToString() }, cancellation);
}
else
throw UnSupported(trackedSource);
return SendAsync<string>(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation);
}
private Exception UnSupported(TrackedSource trackedSource)
@ -316,7 +322,7 @@ namespace NBXplorer
}
public Task<GetBalanceResponse> GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/balance", new[] { CryptoCode, userDerivationScheme.ToString() }, cancellation);
return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation);
}
@ -326,12 +332,30 @@ namespace NBXplorer
}
public Task<GetBalanceResponse> GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/addresses/{1}/balance", new[] { CryptoCode, address.ToString() }, cancellation);
return GetBalanceAsync(TrackedSource.Create(address), cancellation);
}
public Task<GetBalanceResponse> GetBalanceAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation);
}
public async Task<bool> IsTrackedAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
var responseMessage = await SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}", cancellation);
switch (responseMessage.StatusCode)
{
case HttpStatusCode.OK:
return true;
case HttpStatusCode.NotFound:
return false;
default:
await ParseResponse(responseMessage);
return false;
}
}
public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
{
return SendAsync<string>(HttpMethod.Post, keyPaths, "v1/cryptos/{0}/derivations/{1}/addresses/cancelreservation", new[] { CryptoCode, strategy.ToString() }, cancellation);
return SendAsync<string>(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation);
}
public StatusResult GetStatus(CancellationToken cancellation = default)
@ -346,40 +370,39 @@ namespace NBXplorer
{
if (strategy is null)
throw new ArgumentNullException(nameof(strategy));
return SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/wipe", new[] { CryptoCode, strategy.ToString() }, cancellation);
return SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/utxos/wipe", cancellation);
}
public Task<StatusResult> GetStatusAsync(CancellationToken cancellation = default)
{
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", null, cancellation);
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", cancellation);
}
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, CancellationToken cancellation = default)
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
{
return GetTransactionsAsync(strategy, cancellation).GetAwaiter().GetResult();
return GetTransactionsAsync(strategy, from, to, cancellation).GetAwaiter().GetResult();
}
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, CancellationToken cancellation = default)
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
{
return GetTransactionsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
return GetTransactionsAsync(trackedSource, from, to, cancellation).GetAwaiter().GetResult();
}
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
{
return GetTransactionsAsync(TrackedSource.Create(strategy), cancellation);
return GetTransactionsAsync(TrackedSource.Create(strategy), from, to, cancellation);
}
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
string fromV = string.Empty;
string toV = string.Empty;
if (from is DateTimeOffset f)
{
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions", null, cancellation);
fromV = NBitcoin.Utils.DateTimeToUnixTime(f).ToString();
}
else if (trackedSource is AddressTrackedSource asts)
if (to is DateTimeOffset t)
{
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions", null, cancellation);
toV = NBitcoin.Utils.DateTimeToUnixTime(t).ToString();
}
else
throw UnSupported(trackedSource);
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions?from={fromV}&to={toV}", cancellation);
}
@ -404,23 +427,14 @@ namespace NBXplorer
throw new ArgumentNullException(nameof(txId));
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions/{txId}", null, cancellation);
}
else if (trackedSource is AddressTrackedSource asts)
{
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions/{txId}", null, cancellation);
}
else
throw UnSupported(trackedSource);
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation);
}
public Task RescanAsync(RescanRequest rescanRequest, CancellationToken cancellation = default)
{
if (rescanRequest == null)
throw new ArgumentNullException(nameof(rescanRequest));
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", null, cancellation);
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", cancellation);
}
public void Rescan(RescanRequest rescanRequest, CancellationToken cancellation = default)
@ -437,7 +451,7 @@ namespace NBXplorer
{
try
{
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", null, cancellation).ConfigureAwait(false);
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", cancellation).ConfigureAwait(false);
}
catch (NBXplorerException ex) when (ex.Error?.HttpCode == 404)
{
@ -452,16 +466,17 @@ namespace NBXplorer
public async Task<KeyPathInformation> GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/scripts/" + script.ToHex(), new object[] { CryptoCode, strategy }, cancellation).ConfigureAwait(false);
return await GetKeyInformationAsync(new DerivationSchemeTrackedSource(strategy), script, cancellation).ConfigureAwait(false);
}
public async Task<KeyPathInformation> GetKeyInformationAsync(TrackedSource trackedSource, Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
}
[Obsolete("Use GetKeyInformationAsync(DerivationStrategyBase strategy, Script script) instead")]
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, "v1/cryptos/{0}/scripts/" + script.ToHex(), new[] { CryptoCode }, cancellation).ConfigureAwait(false);
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
}
[Obsolete("Use GetKeyInformation(DerivationStrategyBase strategy, Script script) instead")]
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default)
{
return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult();
@ -480,7 +495,7 @@ namespace NBXplorer
{
try
{
return await GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation).ConfigureAwait(false);
return await GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation).ConfigureAwait(false);
}
catch (NBXplorerException ex) when (fallbackFeeRate != null && ex.Error.Code == "fee-estimation-unavailable")
{
@ -489,7 +504,7 @@ namespace NBXplorer
}
public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, CancellationToken cancellation = default)
{
return GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation);
return GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation);
}
public CreatePSBTResponse CreatePSBT(DerivationStrategyBase derivationStrategy, CreatePSBTRequest request, CancellationToken cancellation = default)
{
@ -501,7 +516,7 @@ namespace NBXplorer
throw new ArgumentNullException(nameof(derivationStrategy));
if (request == null)
throw new ArgumentNullException(nameof(request));
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations/{1}/psbt/create", new object[] { CryptoCode, derivationStrategy }, cancellation);
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations/{derivationStrategy}/psbt/create", cancellation);
}
public UpdatePSBTResponse UpdatePSBT(UpdatePSBTRequest request, CancellationToken cancellation = default)
@ -512,7 +527,7 @@ namespace NBXplorer
{
if (request == null)
throw new ArgumentNullException(nameof(request));
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/psbt/update", new object[] { CryptoCode }, cancellation);
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/psbt/update", cancellation);
}
public BroadcastResult Broadcast(Transaction tx, CancellationToken cancellation = default)
{
@ -530,7 +545,7 @@ namespace NBXplorer
public Task<BroadcastResult> BroadcastAsync(Transaction tx, bool testMempoolAccept, CancellationToken cancellation = default)
{
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), "v1/cryptos/{0}/transactions?testMempoolAccept={1}", new[] { CryptoCode, testMempoolAccept.ToString() }, cancellation);
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), $"v1/cryptos/{CryptoCode}/transactions?testMempoolAccept={testMempoolAccept}", cancellation);
}
public TMetadata GetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, CancellationToken cancellationToken = default)
@ -543,7 +558,7 @@ namespace NBXplorer
throw new ArgumentNullException(nameof(derivationScheme));
if (key == null)
throw new ArgumentNullException(nameof(key));
return GetAsync<TMetadata>("v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
return GetAsync<TMetadata>($"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
}
public void SetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
@ -553,13 +568,13 @@ namespace NBXplorer
public Task SetMetadataAsync<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
{
return SendAsync<string>(HttpMethod.Post, value, "v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
return SendAsync<string>(HttpMethod.Post, value, $"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
}
public Task<GenerateWalletResponse> GenerateWalletAsync(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
{
request ??= new GenerateWalletRequest();
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations", new object[] { CryptoCode }, cancellationToken);
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations", cancellationToken);
}
public GenerateWalletResponse GenerateWallet(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
@ -568,6 +583,37 @@ namespace NBXplorer
return GenerateWalletAsync(request, cancellationToken).GetAwaiter().GetResult();
}
public Task<GroupInformation> CreateGroupAsync(CancellationToken cancellationToken = default)
{
return SendAsync<GroupInformation>(HttpMethod.Post, null, $"v1/groups", cancellationToken);
}
public Task<GroupInformation> GetGroupAsync(string groupId, CancellationToken cancellationToken = default)
{
return SendAsync<GroupInformation>(HttpMethod.Get, null, $"v1/groups/{groupId}", cancellationToken);
}
public Task<GroupInformation> AddGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
{
return SendAsync<GroupInformation>(HttpMethod.Post, children, $"v1/groups/{groupId}/children", cancellationToken);
}
public Task<GroupInformation> RemoveGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
{
return SendAsync<GroupInformation>(HttpMethod.Delete, children, $"v1/groups/{groupId}/children", cancellationToken);
}
public Task AddGroupAddressAsync(string cryptoCode, string groupId, string[] addresses, CancellationToken cancellationToken = default)
{
return SendAsync<GroupInformation>(HttpMethod.Post, addresses, $"v1/cryptos/{cryptoCode}/groups/{groupId}/addresses", cancellationToken);
}
public async Task ImportUTXOs(string cryptoCode, ImportUTXORequest request, CancellationToken cancellation = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (cryptoCode == null)
throw new ArgumentNullException(nameof(cryptoCode));
await SendAsync(HttpMethod.Post, request, $"v1/cryptos/{cryptoCode}/rescan-utxos", cancellation);
}
private static readonly HttpClient SharedClient = new HttpClient();
internal HttpClient Client = SharedClient;
@ -611,13 +657,23 @@ namespace NBXplorer
}
}
internal string GetFullUri(string relativePath, params object[] parameters)
static FormattableString EncodeUrlParameters(FormattableString url)
{
return FormattableStringFactory.Create(
url.Format,
url.GetArguments()
.Select(a =>
a is RawStr ? a :
a is FormattableString o ? EncodeUrlParameters(o) :
Uri.EscapeDataString(a?.ToString() ?? ""))
.ToArray());
}
internal string GetFullUri(FormattableString relativePath)
{
relativePath = String.Format(relativePath, parameters ?? new object[0]);
var uri = Address.AbsoluteUri;
if (!uri.EndsWith("/", StringComparison.Ordinal))
uri += "/";
uri += relativePath;
uri += EncodeUrlParameters(relativePath).ToString();
if (!IncludeTransaction)
{
if (uri.IndexOf('?') == -1)
@ -627,13 +683,13 @@ namespace NBXplorer
}
return uri;
}
private Task<T> GetAsync<T>(string relativePath, object[] parameters, CancellationToken cancellation)
private Task<T> GetAsync<T>(FormattableString relativePath, CancellationToken cancellation)
{
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters, cancellation);
return SendAsync<T>(HttpMethod.Get, null, relativePath, cancellation);
}
internal async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, object[] parameters, CancellationToken cancellation)
internal async Task<T> SendAsync<T>(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
{
HttpRequestMessage message = CreateMessage(method, body, relativePath, parameters);
HttpRequestMessage message = CreateMessage(method, body, relativePath);
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
if ((int)result.StatusCode == 404)
{
@ -647,16 +703,35 @@ namespace NBXplorer
{
if (Auth.RefreshCache())
{
message = CreateMessage(method, body, relativePath, parameters);
result = await Client.SendAsync(message).ConfigureAwait(false);
message = CreateMessage(method, body, relativePath);
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
}
}
return await ParseResponse<T>(result).ConfigureAwait(false);
}
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, string relativePath, object[] parameters)
internal async Task<HttpResponseMessage> SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
{
var uri = GetFullUri(relativePath, parameters);
var message = CreateMessage(method, body, relativePath);
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout)
{
throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException());
}
if ((int)result.StatusCode == 401)
{
if (Auth.RefreshCache())
{
message = CreateMessage(method, body, relativePath);
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
}
}
return result;
}
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, FormattableString relativePath)
{
var uri = GetFullUri(relativePath);
var message = new HttpRequestMessage(method, uri);
Auth.SetAuthorization(message);
if (body != null)
@ -709,5 +784,18 @@ namespace NBXplorer
throw error.AsException();
}
}
private FormattableString GetBasePath(TrackedSource trackedSource)
{
if (trackedSource is null)
throw new ArgumentNullException(nameof(trackedSource));
return trackedSource switch
{
DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}",
AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}",
GroupTrackedSource wts => $"v1/cryptos/{CryptoCode}/groups/{wts.GroupId}",
_ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}"
};
}
}
}

View File

@ -37,27 +37,6 @@ namespace NBXplorer
}
}
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index)
{
if((uint)index > (uint)array.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return new ArraySegment<T>(array.Array, array.Offset + index, array.Count - index);
}
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index, int count)
{
if((uint)index > (uint)array.Count || (uint)count > (uint)(array.Count - index))
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return new ArraySegment<T>(array.Array, array.Offset + index, count);
}
public static async Task CloseSocket(this WebSocket socket, WebSocketCloseStatus status, string statusDescription, CancellationToken cancellation = default)
{
try

View File

@ -0,0 +1,54 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using NBXplorer.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace NBXplorer.JsonConverters
{
public class PSBTDestinationJsonConverter : JsonConverter
{
public PSBTDestinationJsonConverter(Network network)
{
Network = network;
}
public Network Network { get; }
public override bool CanConvert(Type objectType)
{
return typeof(PSBTDestination).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
{
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
}
var str = reader.Value.ToString();
try
{
return PSBTDestination.Parse(str, Network);
}
catch (FormatException ex)
{
throw new JsonObjectException(ex.Message, reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is not null)
{
writer.WriteValue(value.ToString());
}
}
}
}

View File

@ -75,7 +75,7 @@ namespace NBXplorer
if (longPolling)
parameters.Add($"longPolling={longPolling}");
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{parametersString}", null, cancellation);
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{ExplorerClient.Raw(parametersString)}", cancellation);
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
.OfType<NewEventBase>()
@ -94,7 +94,7 @@ namespace NBXplorer
if (limit != 10)
parameters.Add($"limit={limit}");
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{parametersString}", null, cancellation);
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{ExplorerClient.Raw(parametersString)}", cancellation);
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
.OfType<NewEventBase>()

View File

@ -1,10 +1,15 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace NBXplorer.Models
{
public class CreatePSBTRequest
{
[JsonProperty("PSBTVersion")]
public int? PSBTVersion { get; set; }
/// <summary>
/// A seed to specific to get a deterministic PSBT (useful for tests)
/// </summary>
@ -63,10 +68,15 @@ namespace NBXplorer.Models
/// </summary>
public List<OutPoint> IncludeOnlyOutpoints { get; set; }
/// <summary>
/// If `true`, all the UTXOs that have been selected will be used as input in the PSBT. (default to false)
/// </summary>
public bool? SpendAllMatchingOutpoints { get; set; }
/// <summary>
/// Use a specific change address (Optional, default: null, mutually exclusive with ReserveChangeAddress)
/// </summary>
public BitcoinAddress ExplicitChangeAddress { get; set; }
public PSBTDestination ExplicitChangeAddress { get; set; }
/// <summary>
/// Rebase the hdkey paths (if no rebase, the key paths are relative to the xpub that NBXplorer knows about)
@ -101,9 +111,66 @@ namespace NBXplorer.Models
/// </summary>
public RootedKeyPath AccountKeyPath { get; set; }
}
public class ScriptDestination : IDestination
{
public ScriptDestination(Script scriptPubKey)
{
ScriptPubKey = scriptPubKey;
}
public Script ScriptPubKey { get; }
}
public abstract class PSBTDestination
{
public static implicit operator PSBTDestination(Script script) => new ScriptType(script);
public static implicit operator PSBTDestination(BitcoinAddress address) => new AddressType(address);
public class ScriptType : PSBTDestination
{
public ScriptType(Script scriptPubKey)
{
if (scriptPubKey is null)
throw new ArgumentNullException(nameof(scriptPubKey));
ScriptPubKey = scriptPubKey;
}
public override Script ScriptPubKey { get; }
public override string ToString() => ScriptPubKey.ToHex();
}
public class AddressType : PSBTDestination
{
public AddressType(BitcoinAddress address)
{
if (address is null)
throw new ArgumentNullException(nameof(address));
Address = address;
}
public BitcoinAddress Address { get; }
public override Script ScriptPubKey => Address.ScriptPubKey;
public override string ToString() => Address.ToString();
}
public abstract Script ScriptPubKey { get; }
public static PSBTDestination Create(Script script) => new ScriptType(script);
public static PSBTDestination Create(BitcoinAddress address) => new AddressType(address);
public static PSBTDestination Parse(string str, Network network)
{
if (str is null)
throw new ArgumentNullException(nameof(str));
if (network is null)
throw new ArgumentNullException(nameof(network));
if (HexEncoder.IsWellFormed(str))
return new ScriptType(Script.FromHex(str));
else
{
return new AddressType(BitcoinAddress.Create(str, network));
}
}
}
public class CreatePSBTDestination
{
public BitcoinAddress Destination { get; set; }
/// <summary>
/// The destination as an address or a script. (in hex)
/// </summary>
public PSBTDestination Destination { get; set; }
/// <summary>
/// Will Send this amount to this destination (Mutually exclusive with: SweepAll)
/// </summary>
@ -120,11 +187,11 @@ namespace NBXplorer.Models
public class FeePreference
{
/// <summary>
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, ExplicitFee, FallbackFeeRate)
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, FallbackFeeRate)
/// </summary>
public FeeRate ExplicitFeeRate { get; set; }
/// <summary>
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, ExplicitFeeRate, FallbackFeeRate)
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, FallbackFeeRate)
/// </summary>
public Money ExplicitFee { get; set; }
/// <summary>

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NBXplorer.Models
@ -14,6 +15,7 @@ namespace NBXplorer.Models
[JsonConverter(typeof(NBXplorer.JsonConverters.ScriptPubKeyTypeConverter))]
public NBitcoin.ScriptPubKeyType? ScriptPubKeyType { get; set; }
public string Passphrase { get; set; }
[Obsolete("We will remove this feature in a future release.")]
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
public Dictionary<string, string> AdditionalOptions { get; set; }

View File

@ -6,6 +6,7 @@ namespace NBXplorer.Models
{
public class GenerateWalletResponse
{
public string TrackedSource { get; set; }
public string Mnemonic { get; set; }
public string Passphrase { get; set; }
[JsonConverter(typeof(NBXplorer.JsonConverters.WordlistJsonConverter))]
@ -17,7 +18,7 @@ namespace NBXplorer.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public NBitcoin.RootedKeyPath AccountKeyPath { get; set; }
public string AccountDescriptor { get; set; }
public DerivationStrategyBase DerivationScheme { get; set; }
public StandardDerivationStrategyBase DerivationScheme { get; set; }
public Mnemonic GetMnemonic()
{

View File

@ -89,10 +89,10 @@ namespace NBXplorer.Models
get; set;
} = new List<MatchedOutput>();
public List<MatchedOutput> Inputs
public List<MatchedInput> Inputs
{
get; set;
} = new List<MatchedOutput>();
} = new List<MatchedInput>();
public DateTimeOffset Timestamp
{
get;
@ -106,5 +106,6 @@ namespace NBXplorer.Models
public uint256 ReplacedBy { get; set; }
public uint256 Replacing { get; set; }
public bool Replaceable { get; set; }
public TransactionMetadata Metadata { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
public class GroupChild
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string CryptoCode { get; set; }
public string TrackedSource { get; set; }
}
public class GroupInformation
{
public string TrackedSource { get; set; }
public string GroupId { get; set; }
public GroupChild[] Children { get; set; }
public GroupChild AsGroupChild() => new () { TrackedSource = TrackedSource };
}
}

View File

@ -0,0 +1,10 @@
using NBitcoin;
using Newtonsoft.Json;
namespace NBXplorer.Models;
public class ImportUTXORequest
{
[JsonProperty("UTXOs")]
public OutPoint[] Utxos { get; set; }
}

View File

@ -1,26 +1,13 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace NBXplorer.Models
{
public class KeyPathInformation
{
public KeyPathInformation()
{
}
public KeyPathInformation(Derivation derivation, DerivationSchemeTrackedSource derivationStrategy, DerivationFeature feature, KeyPath keyPath, NBXplorerNetwork network)
{
ScriptPubKey = derivation.ScriptPubKey;
Redeem = derivation.Redeem;
TrackedSource = derivationStrategy;
DerivationStrategy = derivationStrategy.DerivationStrategy;
Feature = feature;
KeyPath = keyPath;
Address = network.CreateAddress(derivationStrategy.DerivationStrategy, keyPath, ScriptPubKey);
}
public TrackedSource TrackedSource { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public DerivationFeature Feature
@ -48,9 +35,10 @@ namespace NBXplorer.Models
{
get; set;
}
public int GetIndex(KeyPathTemplates keyPathTemplates)
{
return (int)keyPathTemplates.GetKeyPathTemplate(Feature).GetIndex(KeyPath);
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? Index { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -71,25 +71,6 @@ namespace NBXplorer
return true;
}
public bool TryMatchTemplate(KeyPath keyPath, out uint index)
{
index = 0;
if (keyPath.Length != 1 + PreIndexes.Length + PostIndexes.Length)
return false;
for (int i = 0; i < PreIndexes.Length; i++)
{
if (PreIndexes[i] != keyPath[i])
return false;
}
for (int i = 0; i < PostIndexes.Length; i++)
{
if (PostIndexes[i] != keyPath[i + 1 + PreIndexes.Length])
return false;
}
index = keyPath[PreIndexes.Length];
return true;
}
private static bool TryParseCore(string i, out uint index)
{
if (i.Length == 0)
@ -147,12 +128,5 @@ namespace NBXplorer
builder.Append($"/{PostIndexes}");
return builder.ToString();
}
public uint GetIndex(KeyPath keypath)
{
if (TryMatchTemplate(keypath, out var index))
return index;
throw new ArgumentException("Impossible to get the index of this keypath", nameof(keypath));
}
}
}

View File

@ -13,14 +13,7 @@ namespace NBXplorer
private readonly KeyPathTemplate customKeyPathTemplate;
private static readonly KeyPathTemplates _Default = new KeyPathTemplates();
private readonly DerivationFeature[] derivationFeatures;
public static KeyPathTemplates Default
{
get
{
return _Default;
}
}
public static KeyPathTemplates Default => _Default;
private KeyPathTemplates() : this(null)
{
@ -29,81 +22,27 @@ namespace NBXplorer
public KeyPathTemplates(KeyPathTemplate customKeyPathTemplate)
{
this.customKeyPathTemplate = customKeyPathTemplate;
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>();
derivationFeatures.Add(DerivationFeature.Deposit);
derivationFeatures.Add(DerivationFeature.Change);
derivationFeatures.Add(DerivationFeature.Direct);
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>
{
DerivationFeature.Deposit,
DerivationFeature.Change,
DerivationFeature.Direct
};
if (customKeyPathTemplate != null)
derivationFeatures.Add(DerivationFeature.Custom);
this.derivationFeatures = derivationFeatures.ToArray();
}
public KeyPathTemplate GetKeyPathTemplate(DerivationFeature derivationFeature)
{
switch (derivationFeature)
=> derivationFeature switch
{
case DerivationFeature.Deposit:
return depositKeyPathTemplate;
case DerivationFeature.Change:
return changeKeyPathTemplate;
case DerivationFeature.Direct:
return directKeyPathTemplate;
case DerivationFeature.Custom when customKeyPathTemplate != null:
return customKeyPathTemplate;
default:
throw new NotSupportedException(derivationFeature.ToString());
}
}
DerivationFeature.Deposit => depositKeyPathTemplate,
DerivationFeature.Change => changeKeyPathTemplate,
DerivationFeature.Direct => directKeyPathTemplate,
DerivationFeature.Custom when customKeyPathTemplate != null => customKeyPathTemplate,
_ => throw new NotSupportedException($"The derivation feature {derivationFeature} is not supported by the key path templates.")
};
public KeyPathTemplate GetKeyPathTemplate(KeyPath keyPath)
{
if (keyPath == null)
throw new ArgumentNullException(nameof(keyPath));
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return depositKeyPathTemplate;
}
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return changeKeyPathTemplate;
}
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return directKeyPathTemplate;
}
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return customKeyPathTemplate;
}
else
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
}
public DerivationFeature GetDerivationFeature(KeyPath keyPath)
{
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return DerivationFeature.Deposit;
}
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return DerivationFeature.Change;
}
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return DerivationFeature.Direct;
}
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
{
return DerivationFeature.Custom;
}
else
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
}
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures()
{
return derivationFeatures;
}
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures() => derivationFeatures;
}
}

View File

@ -28,6 +28,10 @@ namespace NBXplorer.Models
get; set;
}
public List<MatchedInput> Inputs
{
get; set;
} = new List<MatchedInput>();
public List<MatchedOutput> Outputs
{
get; set;
@ -62,7 +66,19 @@ namespace NBXplorer.Models
public KeyPath KeyPath { get; set; }
public Script ScriptPubKey { get; set; }
public int Index { get; set; }
public int KeyIndex { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public DerivationFeature? Feature { get; set; }
public IMoney Value { get; set; }
public BitcoinAddress Address { get; set; }
}
public class MatchedInput : MatchedOutput
{
public int InputIndex { get; set; }
public uint256 TransactionId
{
get; set;
}
}
}

View File

@ -48,12 +48,6 @@ namespace NBXplorer.Models
{
get; set;
}
public string Backend { get; set; }
public double RepositoryPingTime
{
get;
set;
}
public bool IsFullySynched
{
get; set;

View File

@ -1,6 +1,9 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace NBXplorer.Models
{
@ -10,22 +13,30 @@ namespace NBXplorer.Models
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (network == null)
throw new ArgumentNullException(nameof(network));
trackedSource = null;
var strSpan = str.AsSpan();
if (strSpan.StartsWith("DERIVATIONSCHEME:".AsSpan(), StringComparison.Ordinal))
{
if (network is null)
return false;
if (!DerivationSchemeTrackedSource.TryParse(strSpan, out var derivationSchemeTrackedSource, network))
return false;
trackedSource = derivationSchemeTrackedSource;
}
else if (strSpan.StartsWith("ADDRESS:".AsSpan(), StringComparison.Ordinal))
{
if (network is null)
return false;
if (!AddressTrackedSource.TryParse(strSpan, out var addressTrackedSource, network.NBitcoinNetwork))
return false;
trackedSource = addressTrackedSource;
}
else if (strSpan.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
{
if (!GroupTrackedSource.TryParse(strSpan, out var walletTrackedSource))
return false;
trackedSource = walletTrackedSource;
}
else
{
return false;
@ -97,6 +108,51 @@ namespace NBXplorer.Models
}
}
public class GroupTrackedSource : TrackedSource
{
public string GroupId { get; }
public static GroupTrackedSource Generate()
{
Span<byte> r = stackalloc byte[13];
// 13 is most consistent on number of chars and more than we need to avoid generating twice same id
RandomNumberGenerator.Fill(r);
return new GroupTrackedSource(Encoders.Base58.EncodeData(r));
}
public GroupTrackedSource(string groupId)
{
GroupId = groupId;
}
public static bool TryParse(ReadOnlySpan<char> trackedSource, out GroupTrackedSource walletTrackedSource)
{
walletTrackedSource = null;
if (!trackedSource.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
return false;
try
{
walletTrackedSource = new GroupTrackedSource(trackedSource.Slice("GROUP:".Length).ToString());
return true;
}
catch { return false; }
}
public override string ToString()
{
return "GROUP:" + GroupId;
}
public override string ToPrettyString()
{
return "G:" + GroupId;
}
public static GroupTrackedSource Parse(string trackedSource)
{
return TryParse(trackedSource, out var g) ? g : throw new FormatException("Invalid group tracked source format");
}
}
public class AddressTrackedSource : TrackedSource, IDestination
{
// Note that we should in theory access BitcoinAddress. But parsing BitcoinAddress is very expensive, so we keep storing plain strings
@ -119,8 +175,6 @@ namespace NBXplorer.Models
public static bool TryParse(ReadOnlySpan<char> strSpan, out TrackedSource addressTrackedSource, Network network)
{
if (strSpan == null)
throw new ArgumentNullException(nameof(strSpan));
if (network == null)
throw new ArgumentNullException(nameof(network));
addressTrackedSource = null;
@ -158,8 +212,6 @@ namespace NBXplorer.Models
public static bool TryParse(ReadOnlySpan<char> strSpan, out DerivationSchemeTrackedSource derivationSchemeTrackedSource, NBXplorerNetwork network)
{
if (strSpan == null)
throw new ArgumentNullException(nameof(strSpan));
if (network == null)
throw new ArgumentNullException(nameof(network));
derivationSchemeTrackedSource = null;
@ -188,5 +240,12 @@ namespace NBXplorer.Models
}
return strategy;
}
#if !NO_RECORD
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
=> DerivationStrategy is PolicyDerivationStrategy ? new[] { DerivationFeature.Deposit, DerivationFeature.Change } : keyPathTemplates.GetSupportedDerivationFeatures();
#else
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
=> keyPathTemplates.GetSupportedDerivationFeatures();
#endif
}
}

View File

@ -0,0 +1,40 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
public class TransactionMetadata
{
public class ChunkMetadata
{
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
public Money Fees { get; set; }
[JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Weight { get; set; }
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
[JsonProperty("vsize", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? VirtualSize { get; set; }
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
public Money Fees { get; set; }
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
public ChunkMetadata Chunk { get; set; }
public static TransactionMetadata Parse(string json) => JsonConvert.DeserializeObject<TransactionMetadata>(json);
public string ToString(bool indented) => JsonConvert.SerializeObject(this, indented ? Formatting.Indented : Formatting.None);
public override string ToString() => ToString(true);
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -55,5 +55,7 @@ namespace NBXplorer.Models
set;
}
public uint256 ReplacedBy { get; set; }
public TransactionMetadata Metadata { get; set; }
}
}

View File

@ -4,6 +4,9 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
namespace NBXplorer.Models
{
@ -42,6 +45,11 @@ namespace NBXplorer.Models
}
}
public List<UTXO> SpentUnconfirmed
{
get;
set;
} = new List<UTXO>();
UTXOChange _Confirmed = new UTXOChange();
public UTXOChange Confirmed
@ -85,7 +93,7 @@ namespace NBXplorer.Models
public Key[] GetKeys(ExtKey extKey, bool excludeUnconfirmedUTXOs = false)
{
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Where(u => u.KeyPath is not null).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
}
}
public class UTXOChange
@ -151,16 +159,31 @@ namespace NBXplorer.Models
if (Value is Money v)
{
var coin = new Coin(Outpoint, new TxOut(v, ScriptPubKey));
if (derivationStrategy != null)
if (Redeem is not null)
{
var derivation = derivationStrategy.GetDerivation(KeyPath);
if (derivation.ScriptPubKey != coin.ScriptPubKey)
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
if (derivation.Redeem != null)
coin = coin.ToScriptCoin(derivation.Redeem);
}
else if (Redeem is not null)
coin = coin.ToScriptCoin(Redeem);
}
else
{
DerivationStrategy.Derivation derivation = null;
if (derivationStrategy is StandardDerivationStrategyBase kd && KeyPath is not null)
{
derivation = kd.GetDerivation(KeyPath);
}
#if !NO_RECORD
else if (derivationStrategy is PolicyDerivationStrategy md && Feature is { } f)
{
derivation = md.GetDerivation(f, (uint)KeyIndex);
}
#endif
if (derivation is not null)
{
if (derivation.ScriptPubKey != coin.ScriptPubKey)
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
if (derivation.Redeem != null)
coin = coin.ToScriptCoin(derivation.Redeem);
}
}
return coin;
}
return null;
@ -254,5 +277,7 @@ namespace NBXplorer.Models
_Confirmations = checked((long)value);
}
}
public int KeyIndex { get; set; }
}
}

View File

@ -1,34 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
<Company>Digital Garage</Company>
<Version>4.2.5</Version>
<Version>5.0.6</Version>
<Copyright>Copyright © Digital Garage 2017</Copyright>
<Description>Client API for the minimalist HD Wallet Tracker NBXplorer</Description>
<PackageIconUrl>https://aois.blob.core.windows.net/public/Bitcoin.png</PackageIconUrl>
<PackageIcon>Bitcoin.png</PackageIcon>
<PackageTags>bitcoin</PackageTags>
<PackageProjectUrl>https://github.com/dgarage/NBXplorer/</PackageProjectUrl>
<PackageProjectUrl>https://github.com/btcpayserver/NBXplorer/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/dgarage/NBXplorer</RepositoryUrl>
<RepositoryUrl>https://github.com/btcpayserver/NBXplorer</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<LangVersion>10.0</LangVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<LangVersion>12</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<DefineConstants>$(DefineConstants);NO_SPAN</DefineConstants>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);NO_RECORD</DefineConstants>
</PropertyGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573;1572;1584;1570;3021</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="7.0.31" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.19" />
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
<PackageReference Include="NBitcoin" Version="10.0.6" />
<PackageReference Include="NBitcoin.Altcoins" Version="6.0.3" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="NBXplorer.Tests" />
</ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="Bitcoin.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
</ItemGroup>
</Project>

View File

@ -50,6 +50,7 @@ namespace NBXplorer
internal set;
}
[Obsolete]
public virtual BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
{
return scriptPubKey.GetDestinationAddress(NBitcoinNetwork);

View File

@ -4,6 +4,9 @@ using NBitcoin.Altcoins.Elements;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
namespace NBXplorer
{
@ -23,14 +26,19 @@ namespace NBXplorer
return factory;
}
public BitcoinAddress BlindIfNeeded(DerivationStrategyBase derivationStrategy, BitcoinAddress address, KeyPath keyPath)
{
if (derivationStrategy.Unblinded() || address is BitcoinBlindedAddress)
return address;
var blindingPubKey = GenerateBlindingKey(derivationStrategy, keyPath, address.ScriptPubKey, NBitcoinNetwork).PubKey;
return new BitcoinBlindedAddress(blindingPubKey, address);
}
[Obsolete]
public override BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
{
if (derivationStrategy.Unblinded())
{
return base.CreateAddress(derivationStrategy, keyPath, scriptPubKey);
}
var blindingPubKey = GenerateBlindingKey(derivationStrategy, keyPath, scriptPubKey, NBitcoinNetwork).PubKey;
return new BitcoinBlindedAddress(blindingPubKey, base.CreateAddress(derivationStrategy, keyPath, scriptPubKey));
var addr = scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
return BlindIfNeeded(derivationStrategy, addr, keyPath);
}
public static Key GenerateSlip77BlindingKeyFromMnemonic(Mnemonic mnemonic, Script script)
@ -46,7 +54,7 @@ namespace NBXplorer
return new Key(Hashes.HMACSHA256(masterBlindingKey.ToBytes(), script.ToBytes()));
}
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script script, Network network)
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey, Network network)
{
if (derivationStrategy.Unblinded())
{
@ -57,11 +65,11 @@ namespace NBXplorer
{
if (HexEncoder.IsWellFormed(key))
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), script);
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), scriptPubKey);
}
try
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), script);
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), scriptPubKey);
}
catch (Exception)
{
@ -70,7 +78,7 @@ namespace NBXplorer
try
{
var data = new Mnemonic(key);
return GenerateSlip77BlindingKeyFromMnemonic(data, derivationStrategy.GetDerivation(keyPath).ScriptPubKey);
return GenerateSlip77BlindingKeyFromMnemonic(data, scriptPubKey);
}
catch (Exception)
{
@ -79,10 +87,12 @@ namespace NBXplorer
throw new InvalidOperationException("The key provided for slip77 derivation was invalid.");
}
var blindingKey = new Key(derivationStrategy.GetChild(keyPath).GetChild(new KeyPath("0")).GetDerivation()
.ScriptPubKey.WitHash.ToBytes());
return blindingKey;
else if (derivationStrategy is StandardDerivationStrategyBase kpd && keyPath is not null)
{
var blindingKey = new Key(kpd.GetDerivation(keyPath.Derive(new KeyPath(0))).ScriptPubKey.WitHash.ToBytes());
return blindingKey;
}
throw new InvalidOperationException("-[blinded] doesn't work on miniscript derivation strategies, use [slip77=key] instead");
}
}
private void InitLiquid(ChainName networkType)

View File

@ -0,0 +1,24 @@
using NBitcoin;
using System;
namespace NBXplorer
{
public partial class NBXplorerNetworkProvider
{
private void InitPepecoin(ChainName networkType)
{
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Pepecoin.Instance, networkType)
{
MinRPCVersion = 10000,
ChainLoadingTimeout = TimeSpan.FromHours(1),
ChainCacheLoadingTimeout = TimeSpan.FromMinutes(2),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3434'") : new KeyPath("1'")
});
}
public NBXplorerNetwork GetPEPE()
{
return GetFromCryptoCode(NBitcoin.Altcoins.Pepecoin.Instance.CryptoCode);
}
}
}

View File

@ -13,6 +13,7 @@ namespace NBXplorer
InitBitcore(networkType);
InitLitecoin(networkType);
InitDogecoin(networkType);
InitPepecoin(networkType);
InitBCash(networkType);
InitGroestlcoin(networkType);
InitBGold(networkType);

View File

@ -0,0 +1,128 @@
#nullable enable
using System;
using System.Collections.Generic;
using NBXplorer.DerivationStrategy;
namespace NBitcoin;
public static class NBitcoinNBXplorerExtensions
{
/// <summary>
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
/// </summary>
/// <param name="psbt">The PSBT from which to get the keys</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <param name="accountKeyPath">The account key path</param>
/// <returns>HD Keys matching master root key</returns>
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
RootedKeyPath? accountKeyPath)
{
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
return psbt.HDKeysFor(hd, accountKey, accountKeyPath);
return Array.Empty<PSBTHDKeyMatch>();
}
/// <summary>
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
/// </summary>
/// <param name="psbt">The PSBT from which to get the keys</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <returns>HD Keys matching master root key</returns>
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
=> HDKeysFor(psbt, derivationStrategy, accountKey, null);
/// <summary>
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
/// </summary>
/// <param name="coin">The coins to get the keys from</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <param name="accountKeyPath">The account key path</param>
/// <returns>HD Keys matching master root key</returns>
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
RootedKeyPath? accountKeyPath)
{
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
return coin.HDKeysFor(hd, accountKey, accountKeyPath);
return Array.Empty<PSBTHDKeyMatch>();
}
static IHDScriptPubKey? ToHDScriptPubKey(DerivationStrategyBase derivationStrategy, IHDKey accountKey)
{
if (derivationStrategy is null)
throw new ArgumentNullException(nameof(derivationStrategy));
if (derivationStrategy is StandardDerivationStrategyBase standard)
return standard;
#if !NO_RECORD
else if (derivationStrategy is PolicyDerivationStrategy policy && policy.GetHDScriptPubKey(accountKey) is IHDScriptPubKey hd)
return hd;
#endif
return null;
}
/// <summary>
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
/// </summary>
/// <param name="coin">The coins to get the keys from</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <returns>HD Keys matching master root key</returns>
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
=> HDKeysFor(coin, derivationStrategy, accountKey, null);
/// <summary>
/// Get the balance change if you were signing this transaction.
/// </summary>
/// <param name="psbt">The PSBT from which to get the balance</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <param name="accountKeyPath">The account key path</param>
/// <returns>The balance change</returns>
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
{
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
return psbt.GetBalance(hd, accountKey, accountKeyPath);
return Money.Zero;
}
/// <summary>
/// Get the balance change if you were signing this transaction.
/// </summary>
/// <param name="psbt">The PSBT from which to get the balance</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
/// <returns>The balance change</returns>
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
=> GetBalance(psbt, derivationStrategy, accountKey, null);
/// <summary>
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
/// </summary>
/// <param name="psbt">The PSBT to sign</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key with which to sign</param>
/// <param name="accountKeyPath">The account key path (eg. [masterFP]/49'/0'/0')</param>
/// <returns>The signed PSBT</returns>
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
{
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
return psbt.SignAll(hd, accountKey, accountKeyPath);
return psbt;
}
/// <summary>
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
/// </summary>
/// <param name="psbt">The PSBT to sign</param>
/// <param name="derivationStrategy">The derivation scheme</param>
/// <param name="accountKey">The account key with which to sign</param>
/// <returns>The signed PSBT</returns>
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
=> SignAll(psbt, derivationStrategy, accountKey, null);
}

9
NBXplorer.Client/PushNuget.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
rm -rf "bin/Release/"
dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg
package=$(find ./bin/Release -name "*.nupkg" -type f | head -n 1)
dotnet nuget push "${package[0]}" --source "https://api.nuget.org/v3/index.json" --api-key "$NUGET_API_KEY"
ver=$(basename "${package[0]}" | sed -E 's/NBXplorer\.Client\.([0-9]+(\.[0-9]+){1,3}).*/\1/')
git tag -a "Client/v$ver" -m "Client/$ver"
git push origin "Client/v$ver"

View File

@ -3,6 +3,7 @@ using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using NBXplorer.JsonConverters;
namespace NBXplorer
{
@ -28,6 +29,7 @@ namespace NBXplorer
if (_Network != null)
{
settings.Converters.Insert(0, new JsonConverters.CachedSerializer(_Network));
settings.Converters.Insert(1, new PSBTDestinationJsonConverter(_Network.NBitcoinNetwork));
settings.Converters.Add(new JsonConverters.KeyPathTemplateJsonConverter());
}
ReplaceConverter<NBitcoin.JsonConverters.MoneyJsonConverter>(settings, new NBXplorer.JsonConverters.MoneyJsonConverter());

View File

@ -7,71 +7,16 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin.Protocol;
namespace NBXplorer
{
public class WebsocketNotificationSession : NotificationSessionBase, IDisposable
public class WebsocketNotificationSessionLegacy : WebsocketNotificationSession
{
private readonly ExplorerClient _Client;
public ExplorerClient Client
protected override FormattableString GetConnectPath() => $"v1/cryptos/{_Client.CryptoCode}/connect";
internal WebsocketNotificationSessionLegacy(ExplorerClient client) : base(client)
{
get
{
return _Client;
}
}
internal WebsocketNotificationSession(ExplorerClient client)
{
if(client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
}
internal async Task ConnectAsync(CancellationToken cancellation)
{
var uri = _Client.GetFullUri($"v1/cryptos/{_Client.CryptoCode}/connect", null);
uri = ToWebsocketUri(uri);
WebSocket socket = null;
try
{
socket = await ConnectAsyncCore(uri, cancellation);
}
catch(WebSocketException) // For some reason the ErrorCode is not properly set, so we can check for error 401
{
if(!_Client.Auth.RefreshCache())
throw;
socket = await ConnectAsyncCore(uri, cancellation);
}
JsonSerializerSettings settings = new JsonSerializerSettings();
new Serializer(_Client.Network).ConfigureSerializer(settings);
_MessageListener = new WebsocketMessageListener(socket, settings);
}
private async Task<ClientWebSocket> ConnectAsyncCore(string uri, CancellationToken cancellation)
{
var socket = new ClientWebSocket();
_Client.Auth.SetWebSocketAuth(socket);
try
{
await socket.ConnectAsync(new Uri(uri, UriKind.Absolute), cancellation).ConfigureAwait(false);
}
catch { socket.Dispose(); throw; }
return socket;
}
private static string ToWebsocketUri(string uri)
{
if(uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("https://", "wss://");
if(uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("http://", "ws://");
return uri;
}
WebsocketMessageListener _MessageListener;
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
public void ListenNewBlock(CancellationToken cancellation = default)
{
ListenNewBlockAsync(cancellation).GetAwaiter().GetResult();
@ -128,7 +73,7 @@ namespace NBXplorer
public Task ListenDerivationSchemesAsync(DerivationStrategyBase[] derivationSchemes, CancellationToken cancellation = default)
{
return _MessageListener.Send(new Models.NewTransactionEventRequest() { DerivationSchemes = derivationSchemes.Select(d=>d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
return _MessageListener.Send(new Models.NewTransactionEventRequest() { DerivationSchemes = derivationSchemes.Select(d => d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
}
public void ListenTrackedSources(TrackedSource[] trackedSources, CancellationToken cancellation = default)
@ -141,6 +86,70 @@ namespace NBXplorer
return _MessageListener.Send(new Models.NewTransactionEventRequest() { TrackedSources = trackedSources.Select(d => d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
}
}
public class WebsocketNotificationSession : NotificationSessionBase, IDisposable
{
protected readonly ExplorerClient _Client;
public ExplorerClient Client
{
get
{
return _Client;
}
}
internal WebsocketNotificationSession(ExplorerClient client)
{
if(client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
}
internal async Task ConnectAsync(CancellationToken cancellation)
{
var uri = _Client.GetFullUri(GetConnectPath());
uri = ToWebsocketUri(uri);
WebSocket socket = null;
try
{
socket = await ConnectAsyncCore(uri, cancellation);
}
catch (WebSocketException) // For some reason the ErrorCode is not properly set, so we can check for error 401
{
if (!_Client.Auth.RefreshCache())
throw;
socket = await ConnectAsyncCore(uri, cancellation);
}
JsonSerializerSettings settings = new JsonSerializerSettings();
new Serializer(_Client.Network).ConfigureSerializer(settings);
_MessageListener = new WebsocketMessageListener(socket, settings);
}
protected virtual FormattableString GetConnectPath() => $"v1/cryptos/connect?cryptoCode={_Client.Network.CryptoCode}";
private async Task<ClientWebSocket> ConnectAsyncCore(string uri, CancellationToken cancellation)
{
var socket = new ClientWebSocket();
_Client.Auth.SetWebSocketAuth(socket);
try
{
await socket.ConnectAsync(new Uri(uri, UriKind.Absolute), cancellation).ConfigureAwait(false);
}
catch { socket.Dispose(); throw; }
return socket;
}
private static string ToWebsocketUri(string uri)
{
if(uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("https://", "wss://");
if(uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("http://", "ws://");
return uri;
}
protected WebsocketMessageListener _MessageListener;
public override Task<NewEventBase> NextEventAsync(CancellationToken cancellation = default)
{
return _MessageListener.NextMessageAsync(cancellation);

View File

@ -8,6 +8,8 @@ using Xunit;
using System.Net.Http;
using Xunit.Abstractions;
using NBXplorer.Analytics;
using NBXplorer.DerivationStrategy;
using NBitcoin.Altcoins;
namespace NBXplorer.Tests
{

View File

@ -1,84 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using System.Linq;
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Net;
using System.Net.Sockets;
namespace NBXplorer.Tests
{
public class CustomServer : IDisposable
{
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
public CustomServer()
{
var port = FreeTcpPort();
_Host = new WebHostBuilder()
.Configure(app =>
{
app.Run(req =>
{
while(_Act == null)
{
Thread.Sleep(10);
_Closed.Token.ThrowIfCancellationRequested();
}
_Act(req);
_Act = null;
_Evt.TrySetResult(true);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
})
.UseKestrel()
.UseUrls("http://127.0.0.1:" + port)
.Build();
_Host.Start();
}
public Uri GetUri()
{
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
{
var source = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_Evt = source;
_Act = act;
try
{
_Evt.Task.GetAwaiter().GetResult();
}
catch(TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
}
}
public void Dispose()
{
_Closed.Cancel();
_Host.Dispose();
}
}
}

View File

@ -17,6 +17,8 @@ using System.Runtime.CompilerServices;
using System.IO;
using System.Diagnostics;
using NBXplorer.Client;
using System.Data;
using static NBXplorer.Backend.DbConnectionHelper;
using NBXplorer.Backend;
namespace NBXplorer.Tests
@ -84,7 +86,7 @@ namespace NBXplorer.Tests
await connection.ExecuteAsync("ANALYZE;");
goto retry;
}
Assert.False(true, "Unacceptable response time for " + script);
Assert.Fail("Unacceptable response time for " + script);
}
}
@ -143,6 +145,9 @@ namespace NBXplorer.Tests
await AssertBalance(5.0m);
// Another double spent, but this time having an output
await AssertFetchMatches(conn,
new NewOutRaw[] { new ("t2ss", 0, "a1", 51, "") },
new NewInRaw[] { new ("t2ss", 0, "t1", 1) }, true);
Assert.True(await conn.ExecuteScalarAsync<bool>("CALL fetch_matches ('BTC', " +
"ARRAY[" +
"('t2ss', 0, 'a1', 51, '')" +
@ -155,6 +160,7 @@ namespace NBXplorer.Tests
Assert.Equal(conflict.spent_idx, 1);
Assert.Equal(conflict.replacing_tx_id, "t2ss");
Assert.Equal(conflict.replaced_tx_id, "t2s");
Assert.True(conflict.is_new);
await conn.ExecuteAsync("CALL save_matches('BTC');");
await AssertBalance(5.0m + 51m);
@ -163,8 +169,36 @@ namespace NBXplorer.Tests
{
await conn.QueryFirstAsync($"SELECT * FROM txs WHERE tx_id='{txid}'");
}
await conn.ExecuteScalarAsync("CALL save_matches('BTC')");
// Same match, as previously, but this time is_new should be false
// as the conflict has been detected earlier.
await AssertFetchMatches(conn,
new NewOutRaw[] { new("t2ss", 0, "a1", 51, "") },
new NewInRaw[] { new("t2ss", 0, "t1", 1) }, true);
conflict = await conn.QueryFirstAsync("SELECT * FROM matched_conflicts");
Assert.Equal(conflict.spent_tx_id, "t1");
Assert.Equal(conflict.spent_idx, 1);
Assert.Equal(conflict.replacing_tx_id, "t2ss");
Assert.Equal(conflict.replaced_tx_id, "t2s");
Assert.False(conflict.is_new);
// No matches
await AssertFetchMatches(conn,
new NewOutRaw[] { },
new NewInRaw[] { }, false);
}
private async Task AssertFetchMatches(DbConnection conn, NewOutRaw[] outs, NewInRaw[] ins, bool expectedHasMatches)
{
DynamicParameters parameters = new DynamicParameters();
parameters.Add("in_code", "BTC");
parameters.Add("in_outs", outs);
parameters.Add("in_ins", ins);
parameters.Add("has_match", dbType: System.Data.DbType.Boolean, direction: ParameterDirection.InputOutput);
await conn.QueryAsync<int>("fetch_matches", parameters, commandType: CommandType.StoredProcedure);
Assert.Equal(expectedHasMatches, parameters.Get<bool>("has_match"));
}
[Fact]
public async Task CanCalculateHistogram()

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
FROM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
WORKDIR /source
COPY . .
RUN cd NBXplorer.Tests && dotnet build

View File

@ -57,7 +57,7 @@ namespace NBXplorer.Tests
public static DerivationLine GetLineFor(this DerivationStrategyBase strategy, DerivationFeature feature)
{
return strategy.GetLineFor(KeyPathTemplates.Default.GetKeyPathTemplate(feature));
return strategy.GetLineFor(KeyPathTemplates.Default, feature);
}
static BitcoinAddress Dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.Main);
@ -70,12 +70,6 @@ namespace NBXplorer.Tests
{
return client.EnsureGenerateAsync(blockCount).GetAwaiter().GetResult();
}
public static RPCClient WithCapabilitiesOf(this RPCClient client, RPCClient target)
{
client.Capabilities = target.Capabilities;
return client;
}
}
}

View File

@ -69,20 +69,12 @@ namespace NBXplorer.Tests
public void LogInformation(string msg)
{
if(msg != null)
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLoggerProvider LogProvider
{
get;
set;
if (msg != null)
try
{
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
catch { }
}
}
}

View File

@ -8,18 +8,13 @@ using Xunit.Abstractions;
namespace NBXplorer.Tests
{
public class MaintenanceUtilities
public class MaintenanceUtilities(ITestOutputHelper helper) : UnitTestBase(helper)
{
public MaintenanceUtilities(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLoggerProvider(helper);
}
[Fact]
[Trait("Maintenance", "Maintenance")]
public async Task GenerateFullSchema()
{
using var t = ServerTester.Create();
using var t = CreateTester();
var script = await GenerateDbScript(t);
File.WriteAllText(GetFullSchemaFile(), script);
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net10.0</TargetFramework>
<LangVersion>12</LangVersion>
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -11,10 +11,11 @@
<EmbeddedResource Include="Scripts\generate-whale.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin.TestFramework" Version="3.0.22" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
<PackageReference Include="NBitcoin.TestFramework" Version="5.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@ -29,4 +30,9 @@
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Update="Data\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer.Backend;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@ -44,12 +45,11 @@ namespace NBXplorer.Tests
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(new[] { new KeyValuePair<string, string>("POSTGRES", ServerTester.GetTestPostgres(null, name)) });
services.AddSingleton<IConfiguration>(builder.Build());
services.AddSingleton<RepositoryProvider>();
services.AddSingleton<RepositoryProvider, RepositoryProvider>();
services.AddSingleton<HostedServices.DatabaseSetupHostedService>();
var provider = services.BuildServiceProvider();
_Provider = provider.GetService<RepositoryProvider>();
provider.GetRequiredService<HostedServices.DatabaseSetupHostedService>().StartAsync(default).GetAwaiter().GetResult();
_Provider.StartAsync(default).GetAwaiter().GetResult();
_Repository = _Provider.GetRepository(new NBXplorerNetworkProvider(ChainName.Regtest).GetFromCryptoCode("BTC"));

View File

@ -29,6 +29,12 @@ namespace NBXplorer.Tests
//Network = NBitcoin.Altcoins.Dogecoin.Instance.Regtest;
//RPCStringAmount = false;
//Tests of PEPE are broken because it outpoint locking seems to work differently
//CryptoCode = "PEPE";
//nodeDownloadData = NodeDownloadData.Pepecoin.v1_1_0;
//Network = NBitcoin.Altcoins.Pepecoin.Instance.Regtest;
//RPCStringAmount = false;
//CryptoCode = "DASH";
//nodeDownloadData = NodeDownloadData.Dash.v0_12_2;
//Network = NBitcoin.Altcoins.Dash.Instance.Regtest;
@ -62,7 +68,7 @@ namespace NBXplorer.Tests
//Network = NBitcoin.Altcoins.Viacoin.Instance.Regtest;
//CryptoCode = "GRS";
//nodeDownloadData = NodeDownloadData.Groestlcoin.v25_0;
//nodeDownloadData = NodeDownloadData.Groestlcoin.v29_0;
//Network = NBitcoin.Altcoins.Groestlcoin.Instance.Regtest;
//CryptoCode = "BTX";

View File

@ -1,7 +1,7 @@
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using NBXplorer.Configuration;
using Microsoft.AspNetCore.Hosting;
using NBitcoin;
using NBitcoin.Tests;
using System;
@ -16,9 +16,11 @@ using NBitcoin.RPC;
using System.Net;
using NBXplorer.DerivationStrategy;
using System.Net.Http;
using System.Net.Sockets;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin.WalletPolicies;
using Newtonsoft.Json.Linq;
using NBitcoin.Scripting;
namespace NBXplorer.Tests
{
@ -26,13 +28,14 @@ namespace NBXplorer.Tests
{
private readonly string _Directory;
public static ServerTester Create([CallerMemberNameAttribute] string caller = null)
public static ServerTester Create(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
{
return new ServerTester(caller);
return new ServerTester(logs, caller, true);
}
public static ServerTester CreateNoAutoStart([CallerMemberNameAttribute]string caller = null)
public static ServerTester CreateNoAutoStart(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
{
return new ServerTester(caller, false);
return new ServerTester(logs, caller, false);
}
public void Dispose()
@ -56,11 +59,13 @@ namespace NBXplorer.Tests
get; set;
}
public TesterLogs Logs { get; }
public string Caller { get; }
public ServerTester(string directory, bool autoStart = true)
public ServerTester(TesterLogs logs, string directory, bool autoStart = true)
{
_Name = directory;
SetEnvironment();
Logs = logs;
Caller = directory;
var rootTestData = "TestData";
directory = Path.Combine(rootTestData, directory);
@ -70,11 +75,12 @@ namespace NBXplorer.Tests
if (autoStart)
Start();
}
public RPCWalletType? RPCWalletType
{
get;
set;
} = NBitcoin.Tests.RPCWalletType.Legacy;
}
public void Start()
{
@ -107,16 +113,24 @@ namespace NBXplorer.Tests
throw;
}
}
static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
public int TrimEvents { get; set; } = -1;
public bool UseRabbitMQ { get; set; } = false;
public List<(string key, string value)> AdditionalConfiguration { get; set; } = new List<(string key, string value)>();
public List<string> AdditionalFlags = new List<string>();
internal string PostgresConnectionString;
private void StartNBXplorer()
{
var additionalFlags = new List<string>();
var port = CustomServer.FreeTcpPort();
var port = FreeTcpPort();
List<(string key, string value)> keyValues = new List<(string key, string value)>();
keyValues.Add(("conf", Path.Combine(datadir, "settings.config")));
PostgresConnectionString ??= GetTestPostgres(null, _Name);
@ -130,33 +144,17 @@ namespace NBXplorer.Tests
keyValues.Add(("verbose", "1"));
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcauth", Explorer.GetRPCAuth()));
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcurl", Explorer.CreateRPCClient().Address.AbsoluteUri));
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcdefaultwallet", "default"));
keyValues.Add(("exposerpc", "1"));
keyValues.Add(("rpcnotest", "1"));
keyValues.Add(("trimevents", TrimEvents.ToString()));
keyValues.Add(("mingapsize", "3"));
keyValues.Add(("maxgapsize", "8"));
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}nodeendpoint", $"{Explorer.Endpoint.Address}:{Explorer.Endpoint.Port}"));
keyValues.Add(("asbcnstr", AzureServiceBusTestConfig.ConnectionString));
keyValues.Add(("asbblockq", AzureServiceBusTestConfig.NewBlockQueue));
keyValues.Add(("asbtranq", AzureServiceBusTestConfig.NewTransactionQueue));
keyValues.Add(("asbblockt", AzureServiceBusTestConfig.NewBlockTopic));
keyValues.Add(("asbtrant", AzureServiceBusTestConfig.NewTransactionTopic));
if (UseRabbitMQ)
{
keyValues.Add(("rmqhost", RabbitMqTestConfig.RabbitMqHostName));
keyValues.Add(("rmqvirtual", RabbitMqTestConfig.RabbitMqVirtualHost));
keyValues.Add(("rmquser", RabbitMqTestConfig.RabbitMqUsername));
keyValues.Add(("rmqpass", RabbitMqTestConfig.RabbitMqPassword));
keyValues.Add(("rmqtranex", RabbitMqTestConfig.RabbitMqTransactionExchange));
keyValues.Add(("rmqblockex", RabbitMqTestConfig.RabbitMqBlockExchange));
}
var args = keyValues.SelectMany(kv => new[] { $"--{kv.key}", kv.value }
.Concat(new[] { $"--{CryptoCode.ToLowerInvariant()}hastxindex" }))
var args = keyValues.SelectMany(kv => new[] { $"--{kv.key}", kv.value })
.Concat(AdditionalFlags)
.Concat(additionalFlags).ToArray();
Host = new WebHostBuilder()
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
.UseKestrel()
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
@ -164,13 +162,20 @@ namespace NBXplorer.Tests
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddFilter("NBXplorer.Authentication.BasicAuthenticationHandler", LogLevel.Critical)
.ClearProviders()
.AddProvider(Logs.LogProvider);
})
.UseStartup<Startup>()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseKestrel()
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
.UseStartup<Startup>();
})
.Build();
NBXplorer.Logging.Logs.Configure(Host.Services.GetRequiredService<ILoggerFactory>());
NBXplorerNetwork = ((NBXplorerNetworkProvider)Host.Services.GetService(typeof(NBXplorerNetworkProvider))).GetFromCryptoCode(CryptoCode);
RPC = ((IRPCClients)Host.Services.GetService(typeof(IRPCClients))).Get(NBXplorerNetwork);
RPC = ((RPCClientProvider)Host.Services.GetService(typeof(RPCClientProvider))).Get(NBXplorerNetwork);
var conf = (ExplorerConfiguration)Host.Services.GetService(typeof(ExplorerConfiguration));
Host.Start();
Configuration = conf;
@ -205,10 +210,10 @@ namespace NBXplorer.Tests
public HttpClient HttpClient { get; internal set; }
string datadir;
public void ResetExplorer(bool deleteAll = true)
{
Host.Dispose();
_ = Host.StopAsync();
Host.WaitForShutdown();
if (deleteAll)
{
PostgresConnectionString = null;
@ -231,7 +236,7 @@ namespace NBXplorer.Tests
{
get
{
var address = Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.FirstOrDefault();
var address = Host.GetServerFeatures<IServerAddressesFeature>().Addresses.First();
return new Uri(address);
}
}
@ -262,7 +267,7 @@ namespace NBXplorer.Tests
}
public IWebHost Host
public IHost Host
{
get; set;
}
@ -374,9 +379,11 @@ namespace NBXplorer.Tests
var k = PrivateKeyOf(key, path);
try
{
#pragma warning disable CS0618 // Type or member is obsolete
await RPC.ImportPrivKeyAsync(k).ConfigureAwait(false);
#pragma warning restore CS0618 // Type or member is obsolete
}
catch (RPCException ex) when (ex.RPCCode == RPCErrorCode.RPC_WALLET_ERROR)
catch (RPCException ex) when (ex.RPCCode is RPCErrorCode.RPC_WALLET_ERROR or RPCErrorCode.RPC_METHOD_NOT_FOUND)
{
string[] desc;
if (this.RPC.Capabilities.SupportSegwit)
@ -395,8 +402,8 @@ namespace NBXplorer.Tests
new JArray(
new JObject()
{
["desc"] = OutputDescriptor.AddChecksum(d),
["timestamp"] = this.RPC.Network.Consensus.CoinbaseMaturity
["desc"] = Miniscript.AddChecksum(d),
["timestamp"] = "now"
})
}
}).ConfigureAwait(false);
@ -417,7 +424,7 @@ namespace NBXplorer.Tests
return key.ExtKey.Derive(new KeyPath(path)).Neuter().PubKey.Hash.GetAddress(Network);
}
public BitcoinAddress AddressOf(DerivationStrategyBase scheme, string path)
public BitcoinAddress AddressOf(StandardDerivationStrategyBase scheme, string path)
{
return scheme.GetDerivation(KeyPath.Parse(path)).ScriptPubKey.GetDestinationAddress(Network);
}
@ -426,14 +433,14 @@ namespace NBXplorer.Tests
{
return (DirectDerivationStrategy)CreateDerivationStrategy(pubKey, false);
}
public DerivationStrategyBase CreateDerivationStrategy(ExtPubKey pubKey, bool p2sh)
public StandardDerivationStrategyBase CreateDerivationStrategy(ExtPubKey pubKey, bool p2sh)
{
key = key ?? new ExtKey();
pubKey = pubKey ?? key.Neuter();
string suffix = this.RPC.Capabilities.SupportSegwit ? "" : "-[legacy]";
suffix += p2sh ? "-[p2sh]" : "";
scriptPubKeyType = p2sh ? ScriptPubKeyType.SegwitP2SH : ScriptPubKeyType.Segwit;
return NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
return (StandardDerivationStrategyBase)NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
}
ExtKey key;
ScriptPubKeyType scriptPubKeyType;

View File

@ -1,74 +0,0 @@
namespace NBXplorer.Tests
{
public static class RabbitMqTestConfig
{
//Put your rabbit mq settings here
public static string RabbitMqHostName => "localhost";
public static string RabbitMqVirtualHost => "/";
public static string RabbitMqUsername => "guest";
public static string RabbitMqPassword => "guest";
public static string RabbitMqBlockExchange => "NewBlock";
public static string RabbitMqTransactionExchange => "NewTransaction";
}
public static class AzureServiceBusTestConfig
{
public static string ConnectionString
{
get
{
//Put your service bus connection string here - requires READ / WRITE permissions
return "";
}
}
public static string NewBlockQueue
{
get
{
return "newblock";
}
}
public static string NewBlockTopic
{
get
{
return "newbitcoinblock";
}
}
public static string NewBlockSubscription
{
get
{
return "NewBlock";
}
}
public static string NewTransactionQueue
{
get
{
return "newtransaction";
}
}
public static string NewTransactionTopic
{
get
{
return "newbitcointransaction";
}
}
public static string NewTransactionSubscription
{
get
{
return "NewTransaction";
}
}
}
}

View File

@ -0,0 +1,9 @@
using Xunit.Abstractions;
namespace NBXplorer.Tests;
public class TesterLogs(ITestOutputHelper helper)
{
public XUnitLog Tester { get; } = new XUnitLog(helper) { Name = "Tests" };
public XUnitLoggerProvider LogProvider { get; } = new(helper);
}

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Collections.Generic;
using NBitcoin;
using NBXplorer.Models;
using static NBXplorer.Backend.DbConnectionHelper;
namespace NBXplorer.Tests
{
@ -21,18 +22,20 @@ namespace NBXplorer.Tests
public TrackedTransaction Build()
{
var tx = new TrackedTransaction(new TrackedTransactionKey(_TransactionId, _BlockId, true), _Parent._TrackedSource, null as Coin[], null)
{
Inserted = _TimeStamp,
FirstSeen = _TimeStamp
};
var record = new SaveTransactionRecord(null, _TransactionId, _BlockId, null, null, false, _TimeStamp);
var tx = TrackedTransaction.Create(_Parent._TrackedSource, record);
foreach (var input in _Inputs)
{
tx.SpentOutpoints.Add(input.Coin.Outpoint);
tx.SpentOutpoints.Add(input.Coin.Outpoint, 0);
}
foreach (var output in _Outputs)
{
tx.ReceivedCoins.Add(output.Coin);
tx.MatchedOutputs.Add(new MatchedOutput()
{
Index = (int)output.Coin.Outpoint.N,
Value = output.Coin.Amount,
ScriptPubKey = output.Coin.ScriptPubKey
});
}
return tx;
}

View File

@ -0,0 +1,131 @@
using Dapper;
using NBitcoin;
using NBXplorer.Backend;
using NBXplorer.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace NBXplorer.Tests
{
public partial class UnitTest1
{
[Fact]
public async Task CanCRUDGroups()
{
using var tester = CreateTester();
var g1 = await tester.Client.CreateGroupAsync();
void AssertG1Empty()
{
Assert.NotNull(g1.GroupId);
Assert.NotNull(g1.TrackedSource);
Assert.Equal($"GROUP:{g1.GroupId}", g1.TrackedSource);
Assert.Empty(g1.Children);
}
AssertG1Empty();
g1 = await tester.Client.GetGroupAsync(g1.GroupId);
AssertG1Empty();
Assert.Null(await tester.Client.GetGroupAsync("lol"));
Assert.Null(await tester.Client.AddGroupChildrenAsync("lol", Array.Empty<GroupChild>()));
await AssertNBXplorerException(409, tester.Client.AddGroupChildrenAsync(g1.GroupId, [g1.AsGroupChild()]));
Assert.Null(await tester.Client.AddGroupChildrenAsync(g1.GroupId, [new GroupChild() { TrackedSource = "GROUP:Test" }]));
Assert.Null(await tester.Client.AddGroupChildrenAsync("Test", [new GroupChild() { TrackedSource = g1.TrackedSource }]));
var g2 = await tester.Client.CreateGroupAsync();
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
Assert.NotNull(g1);
// Nothing happen if twice
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
Assert.Equal(g2.TrackedSource, Assert.Single(g1.Children).TrackedSource);
await AssertNBXplorerException(409, tester.Client.AddGroupChildrenAsync(g2.GroupId, [g1.AsGroupChild()]));
g1 = await tester.Client.RemoveGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
AssertG1Empty();
var g3 = await tester.Client.CreateGroupAsync();
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild(), g3.AsGroupChild()]);
Assert.Equal(2, g1.Children.Length);
// Adding address in g2 should add the addresse to g1 but not g3
var addresses = Enumerable.Range(0,10).Select(_ => new Key().GetAddress(ScriptPubKeyType.Legacy, tester.Network).ToString()).ToArray();
await tester.Client.AddGroupAddressAsync("BTC", g2.GroupId, addresses);
// Idempotent
await tester.Client.AddGroupAddressAsync("BTC", g2.GroupId, addresses);
async Task AssertAddresses(GroupInformation g)
{
var groupAddresses = await GetGroupAddressesAsync(tester, "BTC", g.GroupId);
Assert.Equal(groupAddresses.Length, addresses.Length);
foreach (var a in addresses)
{
Assert.Contains(a, groupAddresses);
}
}
await AssertAddresses(g1);
await AssertAddresses(g2);
var g3Addrs = await GetGroupAddressesAsync(tester, "BTC", g3.GroupId);
Assert.Empty(g3Addrs);
// Removing g2 should remove all its addresses
g1 = await tester.Client.RemoveGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
await AssertAddresses(g2);
var g1Addrs = await GetGroupAddressesAsync(tester, "BTC", g1.GroupId);
Assert.Empty(g1Addrs);
await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { TrackedSource= "DERIVATIONSCHEME:tpubDC45vUDsFAAqwYKz5hSLi5yJLNduJzpmTw6QTMRPrwdXURoyL81H8oZAaL8EiwEgg92qgMa9h1bB4Y1BZpy9CTNPfjfxvFcWxeiKBHCqSdc" }]));
await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { CryptoCode="BTC", TrackedSource = "DERIVATIONSCHEME:lol" }]));
}
private async Task<string[]> GetGroupAddressesAsync(ServerTester tester, string code, string groupId)
{
await using var conn = await tester.GetService<DbConnectionFactory>().CreateConnection();
return (await conn.QueryAsync<string>("SELECT s.addr FROM wallets_scripts JOIN scripts s USING (code, script) WHERE code=@code AND wallet_id=@wid", new
{
code = code,
wid = Repository.GetWalletKey(new GroupTrackedSource(groupId)).wid
})).ToArray();
}
[Fact]
public async Task CanAliceAndBobShareWallet()
{
using var tester = CreateTester();
var bobW = tester.Client.GenerateWallet(new GenerateWalletRequest() { ScriptPubKeyType = ScriptPubKeyType.Segwit });
var aliceW = tester.Client.GenerateWallet(new GenerateWalletRequest() { ScriptPubKeyType = ScriptPubKeyType.Segwit });
var shared = await tester.Client.CreateGroupAsync();
await tester.Client.AddGroupChildrenAsync(shared.GroupId, new[] { bobW, aliceW }.Select(w => new GroupChild() { CryptoCode = "BTC", TrackedSource = w.TrackedSource }).ToArray());
var unused = tester.Client.GetUnused(bobW.DerivationScheme, DerivationStrategy.DerivationFeature.Deposit);
var txid = tester.SendToAddress(unused.Address, Money.Coins(1.0m));
var gts = GroupTrackedSource.Parse(shared.TrackedSource);
tester.Notifications.WaitForTransaction(gts, txid);
var balance = await tester.Client.GetBalanceAsync(gts);
Assert.Equal(Money.Coins(1.0m), balance.Unconfirmed);
var txs = await tester.Client.GetTransactionsAsync(gts);
var tx = Assert.Single(txs.UnconfirmedTransactions.Transactions);
Assert.Equal(txid, tx.TransactionId);
Assert.NotNull(tx.Outputs[0].Address);
// Can we track manually added address?
await tester.Client.AddGroupAddressAsync("BTC", shared.GroupId, ["n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa"]);
txid = tester.SendToAddress(BitcoinAddress.Create("n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa", tester.Network), Money.Coins(1.2m));
var txEvt = tester.Notifications.WaitForTransaction(gts, txid);
Assert.Single(txEvt.Outputs);
Assert.NotNull(tx.Outputs[0].Address);
balance = await tester.Client.GetBalanceAsync(gts);
Assert.Equal(Money.Coins(1.0m + 1.2m), balance.Unconfirmed);
}
private async Task<NBXplorerException> AssertNBXplorerException(int httpCode, Task<GroupInformation> task)
{
var ex = await Assert.ThrowsAsync<NBXplorerException>(() => task);
Assert.Equal(httpCode, ex.Error.HttpCode);
return ex;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
using System.Runtime.CompilerServices;
using Xunit.Abstractions;
namespace NBXplorer.Tests;
public class UnitTestBase(ITestOutputHelper helper)
{
public TesterLogs Logs { get; set; } = new TesterLogs(helper);
public ServerTester CreateTester([CallerMemberName] string caller = null) => ServerTester.Create(Logs, caller);
public ServerTester CreateTesterNoAutoStart([CallerMemberName] string caller = null) => ServerTester.CreateNoAutoStart(Logs, caller);
}

View File

@ -17,9 +17,9 @@ services:
- postgres
- pgadmin
postgres:
image: postgres:13
image: postgres:18.1
container_name: nbxplorertests_postgres_1
command: [ "-c", "random_page_cost=1.0", "-c", "shared_preload_libraries=pg_stat_statements" ]
command: [ "-c", "random_page_cost=1.0", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:

View File

@ -1,4 +1,4 @@
#!/bin/sh
set -e
dotnet test --filter "Azure!=Azure&Broker!=RabbitMq&Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null
dotnet test --filter "Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null

View File

@ -1,3 +1,4 @@
{
"parallelizeTestCollections": false
"parallelizeTestCollections": false,
"methodDisplay": "method"
}

View File

@ -14,10 +14,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFiles", "SolutionFi
.dockerignore = .dockerignore
docs\API.md = docs\API.md
.circleci\config.yml = .circleci\config.yml
docker-compose.mutiny.yml = docker-compose.mutiny.yml
docker-compose.regtest.yml = docker-compose.regtest.yml
Dockerfile.linuxamd64 = Dockerfile.linuxamd64
Dockerfile.linuxarm32v7 = Dockerfile.linuxarm32v7
Dockerfile.linuxarm64v8 = Dockerfile.linuxarm64v8
Dockerfile = Dockerfile
global.json = global.json
.circleci\run-tests.sh = .circleci\run-tests.sh
EndProjectSection
EndProject

View File

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=psbt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=NBXplorer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xplorer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -68,16 +68,14 @@ namespace NBXplorer
}
}
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider)
{
this.networks = networks;
this.repositoryProvider = repositoryProvider;
this.keyPathTemplates = keyPathTemplates;
}
Dictionary<NBXplorerNetwork, AddressPool> _AddressPoolByNetwork;
private readonly NBXplorerNetworkProvider networks;
private readonly RepositoryProvider repositoryProvider;
private readonly KeyPathTemplates keyPathTemplates;
public async Task StartAsync(CancellationToken cancellationToken)
{
@ -111,9 +109,10 @@ namespace NBXplorer
var derivationStrategy = (m.TrackedSource as Models.DerivationSchemeTrackedSource)?.DerivationStrategy;
if (derivationStrategy == null)
continue;
foreach (var feature in m.KnownKeyPathMapping.Select(kv => keyPathTemplates.GetDerivationFeature(kv.Value)))
foreach (var feature in m.InOuts.Select(kv => kv.Feature).Distinct())
{
refill.Add(GenerateAddresses(network, derivationStrategy, feature));
if (feature is not null)
refill.Add(GenerateAddresses(network, derivationStrategy, feature.Value));
}
}
return Task.WhenAll(refill.ToArray());

View File

@ -39,13 +39,6 @@ namespace NBXplorer
{
_TxById = new Dictionary<uint256, AnnotatedTransaction>(transactions.Count);
ConfirmedTransactions = new List<AnnotatedTransaction>(transactions.Count);
foreach (var tx in transactions)
{
foreach (var keyPathInfo in tx.KnownKeyPathMapping)
{
_KeyPaths.TryAdd(keyPathInfo.Key, keyPathInfo.Value);
}
}
// Let's remove the dups and let's get the current height of the transactions
foreach (var trackedTx in transactions)
@ -82,7 +75,7 @@ namespace NBXplorer
// No way to have double spent in confirmed transactions
try
{
spentBy.Add(spent, annotatedTransaction.Record.TransactionHash);
spentBy.Add(spent.Outpoint, annotatedTransaction.Record.TransactionHash);
}
catch
{
@ -98,7 +91,7 @@ namespace NBXplorer
HashSet<uint256> toRemove = new HashSet<uint256>();
foreach (var annotatedTransaction in unconfs.Values)
{
foreach (var spent in annotatedTransaction.Record.SpentOutpoints)
foreach (var spent in annotatedTransaction.Record.SpentOutpoints.Select(o => o.Outpoint))
{
// All children of a replaced transaction should be replaced
if (replaced.TryGetValue(spent.Hash, out var parent) && parent.ReplacedBy is uint256)
@ -214,7 +207,7 @@ namespace NBXplorer
// but we don't want user cancelling a chain of transaction
foreach (var parentOutpoint in tx.Record.SpentOutpoints)
{
if (_TxById.TryGetValue(parentOutpoint.Hash, out var parent) && parent.Height is null)
if (_TxById.TryGetValue(parentOutpoint.Outpoint.Hash, out var parent) && parent.Height is null)
{
parent.Replaceable = false;
}
@ -261,17 +254,6 @@ namespace NBXplorer
}
}
public MatchedOutput GetUTXO(OutPoint outpoint)
{
if (_TxById.TryGetValue(outpoint.Hash, out var tx))
{
return tx.Record.GetReceivedOutputs().Where(c => c.Index == outpoint.N).FirstOrDefault();
}
return null;
}
Dictionary<Script, KeyPath> _KeyPaths = new Dictionary<Script, KeyPath>();
Dictionary<uint256, AnnotatedTransaction> _TxById = new Dictionary<uint256, AnnotatedTransaction>();
public AnnotatedTransaction GetByTxId(uint256 txId)
{

View File

@ -9,6 +9,7 @@ namespace NBXplorer
{
this.youngToOld = youngToOld;
}
private static readonly AnnotatedTransactionComparer _Youngness = new AnnotatedTransactionComparer(true);
private static readonly AnnotatedTransactionComparer _Oldness = new AnnotatedTransactionComparer(false);
public static AnnotatedTransactionComparer OldToYoung
{

View File

@ -11,7 +11,7 @@ namespace NBXplorer.Authentication
{
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
{
}

View File

@ -0,0 +1,10 @@
namespace NBXplorer.Backend
{
public enum BitcoinDWaiterState
{
NotStarted,
CoreSynching,
NBXplorerSynching,
Ready
}
}

View File

@ -4,76 +4,60 @@ using NBXplorer.Configuration;
using Npgsql;
using System;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
namespace NBXplorer.Backend
{
public class DbConnectionFactory
public class DbConnectionFactory : IAsyncDisposable
{
public DbConnectionFactory(ILogger<DbConnectionFactory> logger,
IConfiguration configuration,
ExplorerConfiguration conf,
KeyPathTemplates keyPathTemplates)
ExplorerConfiguration conf)
{
Logger = logger;
ExplorerConfiguration = conf;
KeyPathTemplates = keyPathTemplates;
ConnectionString = configuration.GetRequired("POSTGRES");
_DS = CreateDataSourceBuilder(null).Build();
}
public NpgsqlDataSourceBuilder CreateDataSourceBuilder(Action<NpgsqlConnectionStringBuilder> action)
{
var connStrBuilder = new NpgsqlConnectionStringBuilder(ConnectionString);
// Since we create lots of connection in the indexer loop, this saves one round
// trip.
connStrBuilder.NoResetOnClose = true;
// This force connections to recreate, fixing some issues where connection
// take more and more RAM on postgres.
connStrBuilder.ConnectionLifetime = (int)TimeSpan.FromMinutes(10).TotalSeconds;
action?.Invoke(connStrBuilder);
var builder = new NpgsqlDataSourceBuilder(connStrBuilder.ConnectionString);
DbConnectionHelper.Register(builder);
return builder;
}
NpgsqlDataSource _DS;
public string ConnectionString { get; }
public ILogger<DbConnectionFactory> Logger { get; }
public ExplorerConfiguration ExplorerConfiguration { get; }
public KeyPathTemplates KeyPathTemplates { get; }
public Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
{
return CreateConnectionHelper(network, null);
}
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network, Action<NpgsqlConnectionStringBuilder> action)
{
return new DbConnectionHelper(network, await CreateConnection(action), KeyPathTemplates)
return new DbConnectionHelper(network, await CreateConnection())
{
MinPoolSize = ExplorerConfiguration.MinGapSize,
MaxPoolSize = ExplorerConfiguration.MaxGapSize
};
}
public Task<DbConnection> CreateConnection()
public async Task<DbConnection> CreateConnection(CancellationToken cancellationToken = default)
{
return CreateConnection(null);
}
public async Task<DbConnection> CreateConnection(Action<NpgsqlConnectionStringBuilder> action)
{
int maxRetries = 10;
int retries = maxRetries;
retry:
var conn = new NpgsqlConnection(GetConnectionString(action));
try
{
await conn.OpenAsync();
}
catch (PostgresException ex) when (ex.IsTransient && retries > 0)
{
retries--;
await conn.DisposeAsync();
await Task.Delay((maxRetries - retries) * 100);
goto retry;
}
catch
{
conn.Dispose();
throw;
}
return conn;
return await _DS.ReliableOpenConnectionAsync(cancellationToken);
}
private string GetConnectionString(Action<NpgsqlConnectionStringBuilder> action)
public ValueTask DisposeAsync()
{
if (action is null)
return ConnectionString;
NpgsqlConnectionStringBuilder builder = new NpgsqlConnectionStringBuilder(ConnectionString);
action(builder);
return builder.ConnectionString;
return _DS.DisposeAsync();
}
}
}

View File

@ -1,12 +1,15 @@
#nullable enable
using Dapper;
using NBitcoin;
using NBitcoin.RPC;
using NBXplorer.DerivationStrategy;
using Npgsql.TypeMapping;
using Npgsql;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NBXplorer.Backend
@ -14,19 +17,16 @@ namespace NBXplorer.Backend
public class DbConnectionHelper : IDisposable, IAsyncDisposable
{
public DbConnectionHelper(NBXplorerNetwork network,
DbConnection connection,
KeyPathTemplates keyPathTemplates)
DbConnection connection)
{
derivationStrategyFactory = new DerivationStrategyFactory(network.NBitcoinNetwork);
Network = network;
Connection = connection;
KeyPathTemplates = keyPathTemplates;
}
DerivationStrategyFactory derivationStrategyFactory;
public NBXplorerNetwork Network { get; }
public DbConnection Connection { get; }
public KeyPathTemplates KeyPathTemplates { get; }
public int MinPoolSize { get; set; }
public int MaxPoolSize { get; set; }
@ -40,59 +40,28 @@ namespace NBXplorer.Backend
return Connection.DisposeAsync();
}
public record NewOut(uint256 txId, int idx, Script script, IMoney value);
public record NewOut(uint256 txId, int idx, Script script, IMoney value)
{
public static NewOut FromCoin(ICoin c)
=> new(c.Outpoint.Hash, (int)c.Outpoint.N, c.TxOut.ScriptPubKey, c.Amount);
}
public record NewIn(uint256 txId, int idx, uint256 spentTxId, int spentIdx);
public record NewOutRaw(string tx_id, long idx, string script, long value, string asset_id);
public record NewInRaw(string tx_id, long idx, string spent_tx_id, long spent_idx);
internal record OutpointRaw(string tx_id, long idx);
public static void Register(INpgsqlTypeMapper typeMapper)
public static void Register(NpgsqlDataSourceBuilder dsBuilder)
{
typeMapper.MapComposite<NewOutRaw>("new_out");
typeMapper.MapComposite<NewInRaw>("new_in");
typeMapper.MapComposite<OutpointRaw>("outpoint");
typeMapper.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
dsBuilder.MapComposite<NewOutRaw>("new_out");
dsBuilder.MapComposite<NewInRaw>("new_in");
dsBuilder.MapComposite<OutpointRaw>("outpoint");
dsBuilder.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
}
public Task<bool> FetchMatches(IEnumerable<Transaction> txs, SlimChainedBlock slimBlock, Money? minUtxoValue)
public async Task<bool> FetchMatches(MatchQuery matchQuery, CancellationToken cancellationToken)
{
var outCount = txs.Select(t => t.Outputs.Count).Sum();
List<NewOut> outs = new List<NewOut>(outCount);
var inCount = txs.Select(t => t.Inputs.Count).Sum();
List<NewIn> ins = new List<NewIn>(inCount);
foreach (var tx in txs)
{
if (!tx.IsCoinBase)
{
int i = 0;
foreach (var input in tx.Inputs)
{
ins.Add(new NewIn(tx.GetHash(), i, input.PrevOut.Hash, (int)input.PrevOut.N));
i++;
}
}
int io = -1;
foreach (var output in tx.Outputs)
{
io++;
if (minUtxoValue != null && output.Value < minUtxoValue)
continue;
outs.Add(new NewOut(tx.GetHash(), io, output.ScriptPubKey, output.Value));
}
}
return FetchMatches(outs, ins);
}
public async Task<bool> FetchMatches(IEnumerable<NewOut>? newOuts, IEnumerable<NewIn>? newIns)
{
newOuts ??= Array.Empty<NewOut>();
newIns ??= Array.Empty<NewIn>();
newOuts.TryGetNonEnumeratedCount(out int outCount);
newIns.TryGetNonEnumeratedCount(out int inCount);
var outs = new List<NewOutRaw>(outCount);
var ins = new List<NewInRaw>(inCount);
foreach (var o in newOuts)
var outs = new List<NewOutRaw>(matchQuery.Outs.Count);
var ins = new List<NewInRaw>(matchQuery.Ins.Count);
foreach (var o in matchQuery.Outs)
{
long value;
string assetId;
@ -113,17 +82,41 @@ namespace NBXplorer.Backend
}
outs.Add(new NewOutRaw(o.txId.ToString(), o.idx, o.script.ToHex(), value, assetId));
}
foreach (var ni in newIns)
foreach (var ni in matchQuery.Ins)
{
ins.Add(new NewInRaw(ni.txId.ToString(), ni.idx, ni.spentTxId.ToString(), ni.spentIdx));
}
return await Connection.ExecuteScalarAsync<bool>("CALL fetch_matches(@code, @outs, @ins, 'f');", new { code = Network.CryptoCode, outs, ins });
DynamicParameters parameters = new DynamicParameters();
parameters.Add("in_code", Network.CryptoCode);
parameters.Add("in_outs", outs);
parameters.Add("in_ins", ins);
parameters.Add("has_match", dbType: System.Data.DbType.Boolean, direction: ParameterDirection.InputOutput);
var command = new CommandDefinition(
commandText: "fetch_matches",
parameters: parameters,
commandType: CommandType.StoredProcedure,
commandTimeout: ((NpgsqlConnection)Connection).CommandTimeout * 3,
cancellationToken: cancellationToken
);
await Connection.QueryAsync<int>(command);
return parameters.Get<bool>("has_match");
}
public record SaveTransactionRecord(Transaction? Transaction, uint256? Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset? SeenAt)
public record SaveTransactionRecord(Transaction? Transaction, uint256 Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset SeenAt)
{
public static SaveTransactionRecord Create(TrackedTransaction t) => new SaveTransactionRecord(t.Transaction, t.TransactionHash, t.BlockHash, t.BlockIndex, t.BlockHeight, t.IsCoinBase, new DateTimeOffset?(t.FirstSeen));
public static SaveTransactionRecord Create(Transaction? tx = null, uint256? txHash = null, SlimChainedBlock? slimBlock = null, int? blockIndex = null, DateTimeOffset? seenAt = null) => new SaveTransactionRecord(
tx,
txHash ?? tx?.GetHash() ?? throw new ArgumentException("tx or txHash is expected"),
slimBlock?.Hash,
blockIndex,
slimBlock?.Height,
tx?.IsCoinBase is true,
seenAt ?? DateTimeOffset.UtcNow
);
}
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions)
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions, Dictionary<uint256, MempoolEntry> mempoolEntries)
{
var parameters = transactions
.DistinctBy(o => o.Id)
@ -132,25 +125,32 @@ namespace NBXplorer.Backend
{
code = Network.CryptoCode,
blk_id = tx.BlockId?.ToString(),
id = tx.Id?.ToString() ?? tx.Transaction?.GetHash()?.ToString(),
id = tx.Id.ToString(),
raw = tx.Transaction?.ToBytes(),
mempool = tx.BlockId is null,
seen_at = tx.SeenAt,
blk_idx = tx.BlockIndex is int i ? i : 0,
blk_height = tx.BlockHeight,
immature = tx.Immature
immature = tx.Immature,
metadata = mempoolEntries.TryGetValue(tx.Id, out var meta) ? meta.ToTransactionMetadata().ToString(false) : null
})
.Where(o => o.id is not null)
.ToArray();
await Connection.ExecuteAsync("INSERT INTO txs(code, tx_id, raw, immature, seen_at) VALUES (@code, @id, @raw, @immature, COALESCE(@seen_at, CURRENT_TIMESTAMP)) " +
" ON CONFLICT (code, tx_id) " +
" DO UPDATE SET seen_at=LEAST(COALESCE(@seen_at, CURRENT_TIMESTAMP), txs.seen_at), raw = COALESCE(@raw, txs.raw), immature=EXCLUDED.immature", parameters);
await Connection.ExecuteAsync("""
INSERT INTO txs(code, tx_id, raw, immature, seen_at, metadata) VALUES (@code, @id, @raw, @immature, COALESCE(@seen_at, CURRENT_TIMESTAMP), @metadata::JSONB)
ON CONFLICT (code, tx_id)
DO UPDATE SET
seen_at=LEAST(COALESCE(@seen_at, CURRENT_TIMESTAMP), txs.seen_at),
raw = COALESCE(@raw, txs.raw),
immature=EXCLUDED.immature,
metadata=COALESCE(@metadata::JSONB, txs.metadata);
""", parameters);
await Connection.ExecuteAsync("INSERT INTO blks_txs VALUES (@code, @blk_id, @id, @blk_idx) ON CONFLICT DO NOTHING", parameters.Where(p => p.blk_id is not null).AsList());
}
public async Task MakeOrphanFrom(int height)
{
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height });
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height = height });
}
public async Task<Dictionary<OutPoint, TxOut>> GetOutputs(IEnumerable<OutPoint> outPoints)
@ -159,7 +159,7 @@ namespace NBXplorer.Backend
List<OutpointRaw> rawOutpoints = new List<OutpointRaw>(outpointCount);
foreach (var o in outPoints)
rawOutpoints.Add(new OutpointRaw(o.Hash.ToString(), o.N));
Dictionary<OutPoint, TxOut> result = new Dictionary<OutPoint, TxOut>();
var result = new Dictionary<OutPoint, TxOut>();
foreach (var r in await Connection.QueryAsync<(string tx_id, long idx, string script, long value, string asset_id)>(
"SELECT o.tx_id, o.idx, o.script, o.value, o.asset_id FROM unnest(@outpoints) outpoints " +
"JOIN outs o ON code=@code AND o.tx_id=outpoints.tx_id AND o.idx=outpoints.idx",
@ -169,7 +169,7 @@ namespace NBXplorer.Backend
outpoints = rawOutpoints
}))
{
var txout = Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
var txout = this.Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
txout.Value = Money.Satoshis(r.value);
txout.ScriptPubKey = Script.FromHex(r.script);
result.TryAdd(new OutPoint(uint256.Parse(r.tx_id), (uint)r.idx), txout);
@ -206,6 +206,11 @@ namespace NBXplorer.Backend
return null;
return Network.Serializer.ToObject<TMetadata>(result);
}
public async Task<HashSet<uint256>> GetUnconfirmedTxs()
{
var txs = await Connection.QueryAsync<string>("SELECT tx_id FROM txs WHERE code=@code AND mempool IS TRUE;", new { code = Network.CryptoCode });
return new HashSet<uint256>(txs.Select(t => uint256.Parse(t)));
}
public async Task NewBlock(SlimChainedBlock newTip)
{

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