Compare commits

...

138 Commits

Author SHA1 Message Date
thunderbiscuit
f2a29c5b05
chore: build python wheels using manylinux_2_28_x86_64 2024-07-28 08:26:55 -05:00
Matthew
34543311bb
chore: clean up import paths that use blockdata 2024-07-01 12:53:00 -05:00
thunderbiscuit
323eb08350
test: clean up persistence test for bdk-swift 2024-07-01 10:34:06 -04:00
thunderbiscuit
7e5897bd1c
chore: disable default-features on bdk_electrum dependency 2024-06-27 15:25:32 -04:00
thunderbiscuit
ffe0c8c1a4
feat: tighten visibility of inner fields not required by external users 2024-06-27 12:32:50 -04:00
thunderbiscuit
cddd5f25d8
refactor: use tuple struct construction for SqliteStore type 2024-06-27 12:17:00 -04:00
thunderbiscuit
f4366ac49f
refactor: remove unused stale code in errors module 2024-06-27 12:08:53 -04:00
thunderbiscuit
e2abc3620a
fix: add jvm live tests to exclude when using excludeConnectedTests property 2024-06-27 08:52:37 -04:00
thunderbiscuit
c66b48467a
test: add offline tests for wallet new_or_load method 2024-06-27 08:52:20 -04:00
thunderbiscuit
33026108a7
feat: add new_or_load method on wallet type 2024-06-26 13:05:26 -04:00
thunderbiscuit
92d40a9f46
fix: use ring flag on rustls dependency for electrum client 2024-06-26 11:30:02 -04:00
thunderbiscuit
3b89af5a6e
test: update tests for alpha 13 release 2024-06-24 10:15:46 -04:00
thunderbiscuit
f66f8417cf
feat: bump rust libraries to alpha 13 2024-06-24 10:15:30 -04:00
itorod
92aeeab436
chore: bump uniffi to 0.28.0 2024-06-24 13:39:37 +00:00
thunderbiscuit
a2794f25b0
test: fix live tests 2024-06-17 17:37:06 -04:00
thunderbiscuit
1a1920e7e3
chore: bump minimum supported android version to 24 2024-06-17 17:37:06 -04:00
Steve Myers
6642c5808b
chore(swift): rename build script to build-xcframework.sh and update to use xcodebuild tool 2024-06-17 17:30:33 -04:00
Matthew
e97e9b731c
feat: add display trait to descriptor 2024-06-13 10:08:45 -05:00
Matthew
94d31ff7ed
feat: add display trait to mnemonic 2024-06-12 20:10:06 -05:00
Matthew
84f1329e84
feat: add address from_script method 2024-06-11 10:04:15 -05:00
thunderbiscuit
efef60082b
feat: use display trait for string representation of address type 2024-06-10 14:35:03 -04:00
Matthew
53afd9c238
chore: callback to foreign trait 2024-06-07 19:30:03 -05:00
Matthew
b9128b0e6a
chore(swift): add new live tests to justfile and ci workflow 2024-06-06 09:00:57 -05:00
thunderbiscuit
9d3733389d
fix: remove ruby config in uniffi.toml 2024-06-05 21:18:04 -04:00
thunderbiscuit
65702f401b
feat: add sqlite error 2024-06-04 10:51:28 -04:00
thunderbiscuit
19b4e1159a
chore: update rust bdk_wallet to alpha 12
This commit also introduces the sqlite store and removes the flat file
 store
2024-06-04 09:11:32 -04:00
thunderbiscuit
e5e7aba208
chore: use serde_json exported from bdk_bitcoind_rpc 2024-05-29 14:57:01 -04:00
thunderbiscuit
9790d60324
feat: add json_serialize method on psbt type 2024-05-29 14:57:00 -04:00
thunderbiscuit
093eb1fc7e
feat: add json_serialize method on psbt type 2024-05-29 14:57:00 -04:00
thunderbiscuit
5ef2bf8a1e
feat: add fee method on psbt type 2024-05-29 14:57:00 -04:00
thunderbiscuit
7fc8da5451
feat: add psbt error 2024-05-29 14:57:00 -04:00
thunderbiscuit
5d41984377
chore: upgrade uniffi dependency to 0.27.1 2024-05-29 13:55:42 -04:00
Matthew
89f803a753
feat: inspect spks on request types 2024-05-29 12:14:17 -05:00
thunderbiscuit
1f7c782d49
chore: bump development versions to alpha 12 2024-05-27 15:12:13 -04:00
thunderbiscuit
af9346c4eb
docs: fix links in changelog 2024-05-27 15:11:06 -04:00
thunderbiscuit
5cf25a50c4
docs: add alpha 11 changes to changelog 2024-05-27 15:07:50 -04:00
thunderbiscuit
c6174199dd
test: fix swift tests to print amount in sats 2024-05-21 12:06:55 -04:00
thunderbiscuit
9c45254c3e
test: fix live android tests 2024-05-21 11:36:29 -04:00
thunderbiscuit
260a0a65b3
chore: bump library version to alpha 11 2024-05-21 11:35:46 -04:00
thunderbiscuit
72985f14ad
tests: update python tests to use amount type 2024-05-21 10:30:48 -04:00
thunderbiscuit
5e3e24906f
feat: add lock_time method on transaction type 2024-05-16 14:30:47 -04:00
thunderbiscuit
c702894143
feat: add output method on transaction type 2024-05-16 14:28:43 -04:00
thunderbiscuit
ecdd7c239b
feat: add input method on transaction type 2024-05-16 14:03:28 -04:00
thunderbiscuit
ca8a3d0471
refactor: streamline blockchain clients error names 2024-05-16 10:41:04 -04:00
thunderbiscuit
8f4c80cb98
test: add electrum client test 2024-05-16 10:17:51 -04:00
thunderbiscuit
4aec4b0434
feat: add broadcast method on electrum client 2024-05-16 10:17:51 -04:00
thunderbiscuit
1913c45ef9
feat: add sync method on electrum client 2024-05-16 10:17:51 -04:00
thunderbiscuit
815fe5f62d
feat: add full_scan method on electrum client 2024-05-16 10:17:50 -04:00
thunderbiscuit
8d30c86076
feat: add simple electrum client 2024-05-16 10:17:50 -04:00
thunderbiscuit
c88b33473b
test: add memory wallet test 2024-05-16 10:16:58 -04:00
thunderbiscuit
79e7ab73ea
feat: add memory wallet 2024-05-15 14:11:02 -04:00
Matthew
f169b1a52f
chore: standard capitalization in error messages 2024-05-14 16:33:03 -05:00
Matthew
97d9bb6fbf
chore: bump rust bdk to alpha 11 2024-05-14 14:44:26 -05:00
thunderbiscuit
f27bada9c9
test: better messages when tests fail for low balance 2024-05-10 12:45:56 -04:00
thunderbiscuit
1b0b50a954
test: use signet for live tests 2024-05-10 10:51:07 -04:00
thunderbiscuit
6e207802b2
ci: fix live tests workflow for swift library 2024-05-10 10:35:21 -04:00
thunderbiscuit
ac15ed7380
ci: explicitly use rust compiler 1.77.1 on python ci runs 2024-05-10 10:10:43 -04:00
thunderbiscuit
e9a76287c8
feat: expose commit method on wallet type 2024-05-08 12:22:49 -04:00
thunderbiscuit
72b5bfd4c9
feat: add sync and full_scan methods on esplora client 2024-05-07 15:49:50 -04:00
thunderbiscuit
19723240b7
chore: bump rust bdk to alpha 10 2024-05-07 15:46:49 -04:00
Matthew
7d951578d0
refactor: standardize justfile task names and parameters across projects 2024-05-06 14:57:26 -05:00
Matthew
b7fe91b003
refactor(error): alias types from external crates 2024-05-06 13:29:28 -05:00
thunderbiscuit
330dc96b8a
build: migrate uniffi bindings files generation to library mode 2024-05-03 09:43:26 -04:00
thunderbiscuit
5557bb94ea
fix: python test command in justfile 2024-05-03 09:43:20 -04:00
Matthew
9b5b96710e
chore: add from to cannotconnecterror 2024-05-02 13:12:37 -05:00
Matthew
75d155c67a
chore: add error tests 2024-05-01 15:48:26 -05:00
Matthew
6522dfdd26
chore: remove allow_shrinking 2024-05-01 11:31:28 -05:00
Matthew
ebaa6fda2f
feat: add bumpfee finish related error 2024-05-01 11:31:27 -05:00
thunderbiscuit
5e8271e158
refactor: remove pre-1.0 code 2024-04-30 16:13:18 -04:00
thunderbiscuit
431ab90f04
fix: set rust compiler version before running cargo commands in python 2024-04-30 11:46:01 -04:00
thunderbiscuit
d4736a64d1
build: stop build if android ndk root is not defined for android lib 2024-04-30 10:53:15 -04:00
thunderbiscuit
f31678bf37
chore: bump development versions 2024-04-29 11:49:55 -04:00
thunderbiscuit
8130a419f2
fix: adjust errors on keys module 2024-04-29 09:47:37 -04:00
thunderbiscuit
262704751c
feat: add check command to justfile 2024-04-29 09:39:37 -04:00
thunderbiscuit
00a8e1ba8b
refactor: use tuple struct for psbt type 2024-04-29 09:39:37 -04:00
thunderbiscuit
d0514f678e
refactor: use tuple struct for transaction type 2024-04-29 09:39:36 -04:00
thunderbiscuit
f6cc63539d
refactor: use tuple struct for address type 2024-04-29 09:39:36 -04:00
thunderbiscuit
df64a96dd2
refactor: use tuple struct for descriptorpublickey type 2024-04-29 09:39:35 -04:00
thunderbiscuit
4cd6a80ce0
refactor: use tuple struct for descriptorsecretkey type 2024-04-29 09:37:24 -04:00
thunderbiscuit
e48af63fe6
refactor: use tuple struct for Mnemonic type 2024-04-29 09:34:13 -04:00
thunderbiscuit
eff4abcbfb
feat: add weight method on transaction type 2024-04-29 09:20:31 -04:00
Matthew
e1a93379ce
feat: add descriptor key related error 2024-04-26 14:46:40 -05:00
Matthew
e609b57bff
feat: add finish related error 2024-04-25 17:07:20 -05:00
thunderbiscuit
282fcfce0a
refactor: move feerate type to bitcoin module 2024-04-24 21:39:08 -04:00
thunderbiscuit
4dd4e91ccd
fix: clippy warnings 2024-04-24 21:35:18 -04:00
thunderbiscuit
4f8b7f4ba5
build: use correct rustup command for ios library build script 2024-04-24 21:35:18 -04:00
thunderbiscuit
4d737d3393
build: use rust compiler 1.77.1 for all builds 2024-04-24 21:35:18 -04:00
thunderbiscuit
e14124b454
chore: bump dependencies in cargo lockfile 2024-04-24 21:35:12 -04:00
Matthew
0a75fc1279
feat: add derivation path related error 2024-04-24 13:59:23 -05:00
Matthew
6ac386c8df
feat: add mnemonic related error 2024-04-23 14:05:57 -05:00
Matthew
126bc61df6
feat: add apply_update related error 2024-04-23 10:52:46 -05:00
Matthew
c63e7ad392
feat: add sign related error 2024-04-22 12:10:50 -05:00
thunderbiscuit
edea8e8c80
docs: update release workflow 2024-04-19 11:39:03 -04:00
thunderbiscuit
6bc974ed2e
fix: add targetSdk variable to android defaultConfig gradle block 2024-04-19 10:48:40 -04:00
thunderbiscuit
2f7652b979
build: add clean task to all libraries 2024-04-19 10:45:28 -04:00
Matthew
fa17a862ce
feat: add broadcast related error 2024-04-19 08:36:07 -05:00
thunderbiscuit
77cfee718c
feat: add serialize method on Transaction type 2024-04-18 14:40:02 -04:00
thunderbiscuit
aa1c0de244
fix: use alpha 9 types for tests 2024-04-18 14:37:47 -04:00
thunderbiscuit
aee84f9634
fix: remove field name collision on esplora error for JVM 2024-04-17 15:09:32 -04:00
thunderbiscuit
a35fdaee61
chore: activate blocking-https-rustls feature on esplora-client dependency 2024-04-17 15:09:31 -04:00
thunderbiscuit
dccc85d8ff
build: add build command to bdk-ffi directory
This is simply to bring it in line with the other libraries.
2024-04-17 15:09:31 -04:00
thunderbiscuit
f1744d192f
chore: bump bdk version to alpha 9 2024-04-17 15:09:25 -04:00
thunderbiscuit
cacb78f4dc
docs: remove bdk-jvm and bdk-android examples from readmes 2024-04-15 13:36:54 -04:00
thunderbiscuit
54f3235254
feat: expose list_unspent and list_output methods on wallet 2024-04-11 11:45:02 -04:00
thunderbiscuit
806e29b93c
feat: expose get_tx method on wallet type 2024-04-11 11:44:51 -04:00
thunderbiscuit
97a104fd5f
feat: add descriptor related errors 2024-04-10 13:31:28 -04:00
thunderbiscuit
0e617bc986
feat: add psbt related errors 2024-04-10 13:28:56 -04:00
thunderbiscuit
ab2e97e782
feat: add transaction related errors 2024-04-10 13:26:43 -04:00
thunderbiscuit
84f2497aeb
feat: add address related errors 2024-04-10 13:25:35 -04:00
thunderbiscuit
ab87355f9d
docs: update readmes for jvm and android libraries 2024-04-10 11:37:53 -04:00
thunderbiscuit
a40702ebd9
build: fix skipping artifact signature task when publishing locally 2024-04-09 16:15:50 -04:00
thunderbiscuit
0eadff1327
build: small fixes related to gradle 8.7 upgrade 2024-04-09 16:15:08 -04:00
thunderbiscuit
78ef936369
build: specify jdk 17 for all android compilation tasks 2024-04-09 16:14:29 -04:00
thunderbiscuit
7b9e71714f
build: remove deprecated package declaration in android manifest 2024-04-09 16:13:19 -04:00
thunderbiscuit
f698d46392
feat: use bdk names for CanonicalTx and ChainPosition exposed structures 2024-04-09 12:36:35 -04:00
Matthew
ab9763bb58
feat: add transaction details 2024-04-06 21:25:43 -05:00
thunderbiscuit
aa035588a0
chore: bump bdk to alpha 8 and use bitcoin::FeeRate 2024-04-05 13:10:02 -04:00
thunderbiscuit
e774c94998
build: bump gradle wrapper version to 8.7 2024-04-05 13:08:32 -04:00
thunderbiscuit
5e41275f29
fix: use jdk 17 for all publishing tasks 2024-04-05 13:08:32 -04:00
thunderbiscuit
15ac8c8ffb
build: prune unused dependency repositories for android and jvm 2024-04-05 13:08:32 -04:00
thunderbiscuit
358b43c31e
docs: update readme for new kotlin version 2024-04-05 13:08:31 -04:00
thunderbiscuit
08f1957fe0
build: use jdk 17 for android and jvm builds 2024-04-05 13:08:31 -04:00
thunderbiscuit
a8128e5056
chore: bump kotlin version to 1.9.23 2024-04-05 13:08:31 -04:00
thunderbiscuit
b879bf4a50
build: update jvm build tools and configurations 2024-04-05 13:08:31 -04:00
thunderbiscuit
43ff1fb394
build: update android build tools and configurations 2024-04-05 13:08:31 -04:00
Matthew
a8541ecd40
test: add test for wallet creation error 2024-04-03 15:05:33 -05:00
thunderbiscuit
c666bb247a
feat: release python libraries for 3.11 and 3.12 2024-04-03 10:37:08 -04:00
thunderbiscuit
d066007d9c
build: use swift local build script from bdk-swift directory 2024-04-02 21:55:59 -04:00
Matthew
c6d17b77ec
refactor(wallet): use walleterror for error handling in try_get_internal_address 2024-04-02 16:52:02 -05:00
thunderbiscuit
9a996b1a94
docs: mention the just tool and new justfiles in root readme 2024-04-01 15:13:04 -04:00
thunderbiscuit
c68c13fbfa
docs: clean up unecessary docs in root readme 2024-04-01 15:07:36 -04:00
thunderbiscuit
0b851d441f
fix: remove --user flag in python generate scripts 2024-04-01 15:03:57 -04:00
thunderbiscuit
267685bd58
chore: add just files to streamline common tasks 2024-03-28 10:52:43 -04:00
thunderbiscuit
896303f487
docs: fix release issue template 2024-03-27 10:11:18 -04:00
thunderbiscuit
9ea9e9384c
chore: bump snapshot and development versions 2024-03-27 10:04:16 -04:00
thunderbiscuit
2e0cd058f4
docs: add alpha 7 release to changelog 2024-03-27 09:57:58 -04:00
thunderbiscuit
c3313fb922
ci: fix publish to maven central job name 2024-03-27 09:57:22 -04:00
thunderbiscuit
5fc30c6c26
chore: add developer information for jvm, android, and python libraries 2024-03-27 09:45:48 -04:00
100 changed files with 4968 additions and 2312 deletions

View File

@ -6,84 +6,90 @@ labels: 'release'
assignees: '' assignees: ''
--- ---
## Create a new minor release # Part 1: Bump BDK Rust Version
## Bumping BDK Rust Version
1. - [ ] Open a PR with an update to `Cargo.toml` to the new bdk release candidate and ensure all CI workflows run correctly. Fix errors if necessary. 1. - [ ] Open a PR with an update to `Cargo.toml` to the new bdk release candidate and ensure all CI workflows run correctly. Fix errors if necessary.
2. - [ ] Once the new bdk release is out, update the PR to replace the release candidate with the full release and merge. 2. - [ ] Once the new bdk release is out, update the PR to replace the release candidate with the full release and merge.
### Specific Libraries' Workflows # Part 2: Prepare Libraries for Release Branch
#### _Android_
### _Android_
3. - [ ] Update the API docs to reflect the changes in the API 3. - [ ] Update the API docs to reflect the changes in the API
4. - [ ] Delete the `target` directory in bdk-ffi and all previous artifacts to make sure you're building the library from scratch. 4. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root, `lib`, and `plugins`) in the bdk-android directory to make sure you're building the library from scratch.
5. - [ ] Build the library and run the tests, and adjust if necessary. 5. - [ ] Build the library and run the offline and live tests, and adjust them if necessary (note that you'll need an Android emulator running).
1. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root, `lib`, and `plugins`) in bdk-android directory to make sure you're building the library from scratch.
2. - [ ] Build the library and run the offline and live tests, and adjust them if necessary (note that you'll need an Android emulator running).
```shell ```shell
# start an emulator prior to running the tests # start an emulator prior to running the tests
cd ./bdk-android/ cd ./bdk-android/
./gradlew buildAndroidLib just clean
./gradlew connectedAndroidTest just build
just test
``` ```
6. - [ ] Update the readme if necessary 6. - [ ] Update the readme if necessary
1. - [ ] Update the readme if necessary
#### _JVM_ ### _JVM_
7. - [ ] Update the API docs to reflect the changes in the API 7. - [ ] Update the API docs to reflect the changes in the API
8. - [ ] Delete the `target` directory in bdk-ffi and all previous artifacts to make sure you're building the library from scratch 8. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root, `lib`, and `plugins`) in bdk-jvm directory to make sure you're building the library from scratch.
9. - [ ] Build the library and run the tests, and adjust if necessary 9. - [ ] Build the library and run the tests, and adjust if necessary
2. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root, `lib`, and `plugins`) in bdk-android directory to make sure you're building the library from scratch.
3. - [ ] Build the library and run the tests, and adjust if necessary
```shell ```shell
cd ./bdk-jvm/ cd ./bdk-jvm/
./gradlew buildJvmLib just clean
./gradlew test just build
just test
``` ```
10. - [ ] Update the readme if necessary 10. - [ ] Update the readme if necessary
1. - [ ] Update the readme if necessary
#### _Swift_ ### _Swift_
11. - [ ] Run the tests and adjust if necessary
1. - [ ] Run the tests and adjust if necessary 11. - [ ] Delete the `target` directory in bdk-ffi
12. - [ ] Run the tests and adjust if necessary
```shell ```shell
./bdk-swift/build-local-swift.sh
cd ./bdk-swift/ cd ./bdk-swift/
swift test just clean
just build
just test
``` ```
12. - [ ] Update the readme if necessary 13. - [ ] Update the readme if necessary
1. - [ ] Update the readme if necessary
#### _Python_ ### _Python_
13. - [ ] Delete the `dist`, `build`, and `bdkpython.egg-info` and rust `target` directories to make sure you are building the library from scratch without any caches
14. - [ ] Build the library 14. - [ ] Delete the `dist`, `build`, and `bdkpython.egg-info` and rust `target` directories to make sure you are building the library from scratch without any caches
15. - [ ] Build the library
```shell ```shell
cd ./bdk-python/ cd ./bdk-python/
just clean
pip3 install --requirement requirements.txt pip3 install --requirement requirements.txt
bash ./scripts/generate-macos-arm64.sh # run the script for your particular platform bash ./scripts/generate-macos-arm64.sh # run the script for your particular platform
python3 setup.py --verbose bdist_wheel python3 setup.py --verbose bdist_wheel
``` ```
1. - [ ] Run the tests and adjust if necessary 16. - [ ] Run the tests and adjust if necessary
```shell ```shell
pip3 install ./dist/bdkpython-<yourversion>-py3-none-any.whl --force-reinstall pip3 install ./dist/bdkpython-<yourversion>-py3-none-any.whl --force-reinstall
python -m unittest --verbose python -m unittest --verbose
``` ```
1. - [ ] Update the readme and `setup.py` if necessary 17. - [ ] Update the readme and `setup.py` if necessary
18. - [ ] Update the Android, JVM, Python, and Swift libraries as per the _Specific Libraries' Workflows_ section above. Open a single PR on master for all of these changes called `Prepare language bindings libraries for 0.X release`. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/315).
### Release Workflow ## Part 3: Release Workflow
17. - [ ] Update the Android, JVM, Python, and Swift libraries as per the _Specific Libraries' Workflows_ section above. Open a single PR on master for all of these changes called `Prepare language bindings libraries for 0.X release`. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/315).
18. - [ ] Create a new branch off of `master` called `release/version` 19. - [ ] Create a new branch off of `master` called `release/<feature version>`, e.g. `release/0.31`
19. - [ ] Update bdk-android version from `SNAPSHOT` version to release version 20. - [ ] Update bdk-android version from `SNAPSHOT` version to release version
20. - [ ] Update bdk-jvm version from `SNAPSHOT` version to release version 21. - [ ] Update bdk-jvm version from `SNAPSHOT` version to release version
21. - [ ] Update bdk-python version from `.dev` version to release version 22. - [ ] Update bdk-python version from `.dev` version to release version
22. - [ ] Open a PR to that release branch that updates the Android, JVM, and Python libraries' versions in step 19, 20, and 21. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/316). 23. - [ ] Open a PR to that release branch that updates the Android, JVM, and Python libraries' versions in the three steps above. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/316).
23. - [ ] Get a review and ACK and merge the PR updating all the languages to their release versions 24. - [ ] Get a review and ACK and merge the PR updating all the languages to their release versions
24. - [ ] Create the tag for the release and make sure to add the changelog info to the tag (works better if you prepare the tag message on the side in a text editor). Push the tag to GitHub. 25. - [ ] Create the tag for the release and make sure to add the changelog info to the tag (works better if you prepare the tag message on the side in a text editor). Push the tag to GitHub.
```shell ```shell
git tag v0.6.0 --sign --edit git tag v0.6.0 --sign --edit
git push upstream v0.6.0 git push upstream v0.6.0
``` ```
25. - [ ] Trigger manual releases for all 4 libraries (for Swift, trigger the release on `master` and simply add the version number in the text field when running the workflow manually. Note that the version number must not contain the `v`, i.e. `0.26.0`) 26. - [ ] Trigger manual releases for all 4 libraries (for Swift, go on the [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) trigger the release on `master` and simply add the version number and tag name in the text fields when running the workflow manually. Note that the version number must not contain the `v`, i.e. `0.26.0`, but the tag will have it, i.e. `v0.26.0`).
26. - [ ] Make sure the released libraries work and contain the artifacts you would expect 27. - [ ] Make sure the released libraries work and contain the artifacts you would expect
27. - [ ] Aggregate all the changelog notices from the PRs and add them to the changelog file 28. - [ ] Aggregate all the changelog notices from the PRs and add them to the changelog file
28. - [ ] Bump the versions on master from `0.9.0-SNAPSHOT` to `0.10.0-SNAPSHOT`, `0.6.0.dev0` to `0.7.0.dev0` 29. - [ ] Bump the versions on master from `0.9.0-SNAPSHOT` to `0.10.0-SNAPSHOT`, `0.6.0.dev0` to `0.7.0.dev0`
29. - [ ] Apply changes to the minor_release and patch_release issue templates if they need any 30. - [ ] Apply changes to the minor_release and patch_release issue templates if they need any
30. - [ ] Open a PR on master with the changes in steps 29, 30, and 31. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/317). Get a review and merge the PR. 31. - [ ] Open a PR on master with the changes in steps 29, 30, and 31. See [example PR here](https://github.com/bitcoindevkit/bdk-ffi/pull/317). Get a review and merge the PR.
31. - [ ] Make release on GitHub (set as pre-release and generate auto release notes between the previous tag and the new one) 32. - [ ] Make release on GitHub (set as pre-release and generate auto release notes between the previous tag and the new one)
32. - [ ] Post in the announcement channel 33. - [ ] Post in the announcement channel
33. - [ ] Tweet about the library 34. - [ ] Tweet about the library

View File

@ -17,7 +17,7 @@ jobs:
strategy: strategy:
matrix: matrix:
rust: rust:
- version: 1.73.0 - version: 1.77.1
clippy: true clippy: true
steps: steps:
- name: "Checkout" - name: "Checkout"

View File

@ -27,8 +27,8 @@ jobs:
distribution: temurin distribution: temurin
java-version: 11 java-version: 11
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Build bdk-jvm library" - name: "Build bdk-jvm library"
run: | run: |
@ -48,7 +48,8 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: "Build Swift package" - name: "Build Swift package"
run: bash ./bdk-swift/build-local-swift.sh working-directory: bdk-swift
run: bash ./build-xcframework.sh
- name: "Run live Swift tests" - name: "Run live Swift tests"
working-directory: bdk-swift working-directory: bdk-swift
@ -74,9 +75,11 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: actions-rs/toolchain@v1
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: 1.77.1
- name: "Generate bdk.py and binaries" - name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh run: bash ./scripts/generate-linux.sh

View File

@ -23,10 +23,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Install Rust Android targets" - name: "Install Rust Android targets"
run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi
@ -36,7 +36,7 @@ jobs:
cd bdk-android cd bdk-android
./gradlew buildAndroidLib ./gradlew buildAndroidLib
- name: "Publish to Maven Local and Maven Central" - name: "Publish to Maven Central"
env: env:
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }}

View File

@ -22,10 +22,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Install aarch64 Rust target" - name: "Install aarch64 Rust target"
run: rustup target add aarch64-apple-darwin run: rustup target add aarch64-apple-darwin
@ -52,10 +52,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Install x86_64-pc-windows-msvc Rust target" - name: "Install x86_64-pc-windows-msvc Rust target"
run: rustup target add x86_64-pc-windows-msvc run: rustup target add x86_64-pc-windows-msvc
@ -92,10 +92,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Build bdk-jvm library" - name: "Build bdk-jvm library"
run: | run: |

View File

@ -1,22 +1,17 @@
name: Publish bdkpython to PyPI name: Publish bdkpython to PyPI
on: [workflow_dispatch] on: [workflow_dispatch]
# We use manylinux2014 because older CentOS versions used by 2010 and 1 have a very old glibc version, which
# makes it very hard to use GitHub's javascript actions (checkout, upload-artifact, etc).
# They mount their own nodejs interpreter inside your container, but since that's not statically linked it
# tries to load glibc and fails because it requires a more recent version.
jobs: jobs:
build-manylinux2014-x86_64-wheels: build-manylinux_2_28-x86_64-wheels:
name: "Build Manylinux 2014 x86_64 wheel" name: "Build Manylinux 2.28 x86_64 wheel"
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
defaults: defaults:
run: run:
working-directory: bdk-python working-directory: bdk-python
container: container:
image: quay.io/pypa/manylinux2014_x86_64 image: quay.io/pypa/manylinux_2_28_x86_64
env: env:
PLAT: manylinux2014_x86_64 PLAT: manylinux_2_28_x86_64
PYBIN: "/opt/python/${{ matrix.python }}/bin" PYBIN: "/opt/python/${{ matrix.python }}/bin"
strategy: strategy:
matrix: matrix:
@ -24,27 +19,30 @@ jobs:
- cp38-cp38 - cp38-cp38
- cp39-cp39 - cp39-cp39
- cp310-cp310 - cp310-cp310
- cp311-cp311
- cp312-cp312
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
# TODO 2: Other CI workflows use explicit Rust compiler versions, I think we should do the same here
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
with:
toolchain: 1.77.1
- name: "Generate bdk.py and binaries" - name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh run: bash ./scripts/generate-linux.sh
- name: "Build wheel" - name: "Build wheel"
# Specifying the plat-name argument is necessary to build a wheel with the correct name, # Specifying the plat-name argument is necessary to build a wheel with the correct name,
# see issue #350 for more information # see issue #350 for more information
run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_17_x86_64 --verbose run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_28_x86_64 --verbose
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: bdkpython-manylinux2014-x86_64-${{ matrix.python }} name: bdkpython-manylinux_2_28_x86_64-${{ matrix.python }}
path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl
build-macos-arm64-wheels: build-macos-arm64-wheels:
@ -59,6 +57,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -96,6 +96,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -132,6 +134,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -159,7 +163,7 @@ jobs:
defaults: defaults:
run: run:
working-directory: bdk-python working-directory: bdk-python
needs: [build-manylinux2014-x86_64-wheels, build-macos-arm64-wheels, build-macos-x86_64-wheels, build-windows-wheels] needs: [build-manylinux_2_28-x86_64-wheels, build-macos-arm64-wheels, build-macos-x86_64-wheels, build-windows-wheels]
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@ -35,10 +35,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Install Rust Android targets" - name: "Install Rust Android targets"
run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi

View File

@ -30,10 +30,10 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: temurin distribution: temurin
java-version: 11 java-version: 17
- name: "Set default Rust version to 1.73.0" - name: "Set default Rust version to 1.77.1"
run: rustup default 1.73.0 run: rustup default 1.77.1
- name: "Run JVM tests" - name: "Run JVM tests"
run: | run: |

View File

@ -10,22 +10,17 @@ on:
- "bdk-ffi/**" - "bdk-ffi/**"
- "bdk-python/**" - "bdk-python/**"
# We use manylinux2014 because older CentOS versions used by 2010 and 1 have a very old glibc version, which
# makes it very hard to use GitHub's javascript actions (checkout, upload-artifact, etc).
# They mount their own nodejs interpreter inside your container, but since that's not statically linked it
# tries to load glibc and fails because it requires a more recent version.
jobs: jobs:
build-manylinux2014-x86_64-wheels: build-manylinux_2_28-x86_64-wheels:
name: "Build and test Manylinux 2014 x86_64 wheels" name: "Build and test Manylinux 2.28 x86_64 wheels"
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
defaults: defaults:
run: run:
working-directory: bdk-python working-directory: bdk-python
container: container:
image: quay.io/pypa/manylinux2014_x86_64 image: quay.io/pypa/manylinux_2_28_x86_64
env: env:
PLAT: manylinux2014_x86_64 PLAT: manylinux_2_28_x86_64
PYBIN: "/opt/python/${{ matrix.python }}/bin" PYBIN: "/opt/python/${{ matrix.python }}/bin"
strategy: strategy:
matrix: matrix:
@ -33,22 +28,26 @@ jobs:
- cp38-cp38 - cp38-cp38
- cp39-cp39 - cp39-cp39
- cp310-cp310 - cp310-cp310
- cp311-cp311
- cp312-cp312
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
with:
toolchain: 1.77.1
- name: "Generate bdk.py and binaries" - name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh run: bash ./scripts/generate-linux.sh
- name: "Build wheel" - name: "Build wheel"
# Specifying the plat-name argument is necessary to build a wheel with the correct name, # Specifying the plat-name argument is necessary to build a wheel with the correct name,
# see issue #350 for more information # see issue #350 for more information
run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_17_x86_64 --verbose run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_28_x86_64 --verbose
- name: "Install wheel" - name: "Install wheel"
run: ${PYBIN}/pip install ./dist/*.whl run: ${PYBIN}/pip install ./dist/*.whl
@ -59,7 +58,7 @@ jobs:
- name: "Upload artifact test" - name: "Upload artifact test"
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: bdkpython-manylinux2014-x86_64-${{ matrix.python }} name: bdkpython-manylinux_2_28_x86_64-${{ matrix.python }}
path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl
build-macos-arm64-wheels: build-macos-arm64-wheels:
@ -74,6 +73,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -117,6 +118,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -158,6 +161,8 @@ jobs:
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11"
- "3.12"
steps: steps:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@ -19,8 +19,9 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: "Build Swift package" - name: "Build Swift package"
run: bash ./bdk-swift/build-local-swift.sh working-directory: bdk-swift
run: bash ./build-xcframework.sh
- name: "Run Swift tests" - name: "Run Swift tests"
working-directory: bdk-swift working-directory: bdk-swift
run: swift test --skip LiveWalletTests --skip LiveTxBuilderTests run: swift test --skip LiveElectrumClientTests --skip LiveMemoryWalletTests --skip LiveTransactionTests --skip LiveTxBuilderTests --skip LiveWalletTests

2
.gitignore vendored
View File

@ -31,6 +31,8 @@ bdkFFI.h
BitcoinDevKit.swift BitcoinDevKit.swift
bdk.swift bdk.swift
.build .build
*.xcframework/
Info.plist
# Python related # Python related
__pycache__ __pycache__

View File

@ -3,7 +3,27 @@ Changelog information can also be found in each release's git tag (which can be
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0-alpha.2a] ## [v1.0.0-alpha.11]
This release brings the latest alpha 11 release of the Rust bdk_wallet library, as well as the new Electrum client, the new memory wallet, and a whole lot of new types and APIs across the library. Also of note are the much simpler-to-use full_scan and sync workflows for syncing wallets.
Added:
- `Amount` type [#533]
- `TxIn` type [#536]
- `Transaction.input()` method [#536]
- `Transaction.output()` method [#536]
- `Transaction.lock_time()` method [#536]
- `Electrum` client [#535]
- Memory wallet [#528]
[#528]: https://github.com/bitcoindevkit/bdk-ffi/pull/528
[#533]: https://github.com/bitcoindevkit/bdk-ffi/pull/533
[#535]: https://github.com/bitcoindevkit/bdk-ffi/pull/535
[#536]: https://github.com/bitcoindevkit/bdk-ffi/pull/536
## [v1.0.0-alpha.7]
This release brings back into the 1.0 API a number of APIs from the 0.31 release, and adds the new flat file persistence feature, as well as more fine-grain errors.
## [v1.0.0-alpha.2a]
This release is the first alpha release of the 1.0 API for the bindings libraries. Here is what is now available: This release is the first alpha release of the 1.0 API for the bindings libraries. Here is what is now available:
- Create and recover wallets using descriptors, including the four descriptor templates - Create and recover wallets using descriptors, including the four descriptor templates
- Sync a wallet using a blocking Esplora client - Sync a wallet using a blocking Esplora client
@ -11,7 +31,7 @@ This release is the first alpha release of the 1.0 API for the bindings librarie
- Create and sign transactions using the transaction builder - Create and sign transactions using the transaction builder
- Broadcast transactions - Broadcast transactions
## [0.31.0] ## [v0.31.0]
This release updates the bindings libraries to bdk version 0.29.0, updating rust-bitcoin to version 0.30.2. This release updates the bindings libraries to bdk version 0.29.0, updating rust-bitcoin to version 0.30.2.
- APIs Changed: - APIs Changed:
@ -23,7 +43,7 @@ This release updates the bindings libraries to bdk version 0.29.0, updating rust
[#443]: https://github.com/bitcoindevkit/bdk-ffi/pull/443 [#443]: https://github.com/bitcoindevkit/bdk-ffi/pull/443
## [0.30.0] ## [v0.30.0]
This release has a new API and a few internal optimizations and refactorings. This release has a new API and a few internal optimizations and refactorings.
- APIs Added - APIs Added
@ -31,7 +51,7 @@ This release has a new API and a few internal optimizations and refactorings.
[#388]: https://github.com/bitcoindevkit/bdk-ffi/pull/388 [#388]: https://github.com/bitcoindevkit/bdk-ffi/pull/388
## [0.29.0] ## [v0.29.0]
This release has a number of new APIs, and adds support for Windows in bdk-jvm. This release has a number of new APIs, and adds support for Windows in bdk-jvm.
Changelog Changelog
@ -242,6 +262,9 @@ Changelog
[BIP 0174]:https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#encoding [BIP 0174]:https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#encoding
[v1.0.0-alpha.11]: https://github.com/bitcoindevkit/bdk-ffi/compare/v1.0.0-alpha.7...v1.0.0-alpha.11
[v1.0.0-alpha.7]: https://github.com/bitcoindevkit/bdk-ffi/compare/v1.0.0-alpha.2a...v1.0.0-alpha.7
[v1.0.0-alpha.2a]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.31.0...v1.0.0-alpha.2a
[v0.31.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.30.0...v0.31.0 [v0.31.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.30.0...v0.31.0
[v0.30.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.29.0...v0.30.0 [v0.30.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.29.0...v0.30.0
[v0.29.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.28.0...v0.29.0 [v0.29.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.28.0...v0.29.0

View File

@ -26,22 +26,21 @@ The below directories (a separate repository in the case of bdk-swift) include i
| Swift | iOS, macOS | [bdk-swift (GitHub)] | [Readme bdk-swift] | | | Swift | iOS, macOS | [bdk-swift (GitHub)] | [Readme bdk-swift] | |
| Python | linux, macOS, Windows | [bdk-python (PyPI)] | [Readme bdk-python] | | | Python | linux, macOS, Windows | [bdk-python (PyPI)] | [Readme bdk-python] | |
## Minimum Supported Rust Version (MSRV) ## Building and Testing the Libraries
If you are familiar with the build tools for the specific languages you wish to build the libraries for, you can use their normal build/test workflows. We also include some [just](https://just.systems/) files to simplify the work across different languages. If you have the just tool installed on your system, you can simply call the commands defined in the `justfile`s, for example:
```sh
cd bdk-android
This library should compile with any combination of features with Rust 1.73.0. just build
just offlinetests
just publishlocal
```
## Minimum Supported Rust Version (MSRV)
This library should compile with any combination of features with Rust 1.77.1.
## Contributing ## Contributing
To add new structs and functions, see the [UniFFI User Guide](https://mozilla.github.io/uniffi-rs/) and the [uniffi-examples](https://thunderbiscuit.github.io/uniffi-examples/) repository.
### Adding new structs and functions
See the [UniFFI User Guide](https://mozilla.github.io/uniffi-rs/)
#### For pass by value objects
1. Create new rust struct with only fields that are supported UniFFI types
2. Update mapping `bdk.udl` file with new `dictionary`
#### For pass by reference values
1. Create wrapper rust struct/impl with only fields that are `Sync + Send`
2. Update mapping `bdk.udl` file with new `interface`
## Goals ## Goals
1. Language bindings should feel idiomatic in target languages/platforms 1. Language bindings should feel idiomatic in target languages/platforms

View File

@ -13,33 +13,6 @@ dependencies {
} }
``` ```
You may then import and use the `org.bitcoindevkit` library in your Kotlin code like so. Note that this example is for the `0.30.0` release. For examples of the 1.0 API in the alpha releases, take a look at the tests [here](https://github.com/bitcoindevkit/bdk-ffi/tree/master/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit).
```kotlin
import org.bitcoindevkit.*
// ...
val externalDescriptor = Descriptor("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", Network.TESTNET)
val internalDescriptor = Descriptor("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)", Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/")
val wallet: Wallet = Wallet(
descriptor = externalDescriptor,
changeDescriptor = internalDescriptor,
persistenceBackendPath = "./bdkwallet.db",
network = Network.TESTNET
)
val update = esploraClient.fullScan(
wallet = wallet,
stopGap = 10uL,
parallelRequests = 1uL
)
wallet.applyUpdate(update)
val newAddress = wallet.getAddress(AddressIndex.LastUnused)
```
### Snapshot releases ### Snapshot releases
To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block: To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block:
```kotlin ```kotlin
@ -58,29 +31,34 @@ dependencies {
* [Padawan Wallet](https://github.com/thunderbiscuit/padawan-wallet) * [Padawan Wallet](https://github.com/thunderbiscuit/padawan-wallet)
### How to build ### How to build
_Note that Kotlin version `1.6.10` or later is required to build the library._ _Note that Kotlin version `1.9.23` or later is required to build the library._
1. Clone this repository. 1. Clone this repository.
```shell ```shell
git clone https://github.com/bitcoindevkit/bdk-ffi git clone https://github.com/bitcoindevkit/bdk-ffi
``` ```
2. Follow the "General" bdk-ffi ["Getting Started (Developer)"] instructions. 2. Follow the "General" bdk-ffi ["Getting Started (Developer)"] instructions.
3. Install Rust (note that we are currently building using Rust 1.73.0): 3. Install Rust (note that we are currently building using Rust 1.77.1):
```shell ```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default 1.73.0 rustup default 1.77.1
``` ```
4. Install required targets 4. Install required targets
```sh ```sh
rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi
``` ```
5. Install Android SDK and Build-Tools for API level 30+ 5. Install Android SDK and Build-Tools for API level 30+
6. Setup `$ANDROID_SDK_ROOT` and `$ANDROID_NDK_ROOT` path variables (which are required by the 6. Setup `ANDROID_SDK_ROOT` and `ANDROID_NDK_ROOT` path variables which are required by the build tool. Note that currently, NDK version 25.2.9519653 or above is required. For example:
build tool), for example (note that currently, NDK version 25.2.9519653 or above is required):
```shell ```shell
export ANDROID_SDK_ROOT=~/Android/Sdk # macOS
export ANDROID_SDK_ROOT=~/Library/Android/sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653
# linux
export ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653 export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653
``` ```
7. Build kotlin bindings 7. Build kotlin bindings
```sh ```sh
# build Android library # build Android library
@ -95,7 +73,7 @@ export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653
## How to publish to your local Maven repo ## How to publish to your local Maven repo
```shell ```shell
cd bdk-android cd bdk-android
./gradlew publishToMavenLocal --exclude-task signMavenPublication ./gradlew publishToMavenLocal -P localBuild
``` ```
Note that the commands assume you don't need the local libraries to be signed. If you do wish to sign them, simply set your `~/.gradle/gradle.properties` signing key values like so: Note that the commands assume you don't need the local libraries to be signed. If you do wish to sign them, simply set your `~/.gradle/gradle.properties` signing key values like so:
@ -104,7 +82,7 @@ signing.gnupg.keyName=<YOUR_GNUPG_ID>
signing.gnupg.passphrase=<YOUR_GNUPG_PASSPHRASE> signing.gnupg.passphrase=<YOUR_GNUPG_PASSPHRASE>
``` ```
and use the `publishToMavenLocal` task without excluding the signing task: and use the `publishToMavenLocal` task without the `localBuild` flag:
```shell ```shell
./gradlew publishToMavenLocal ./gradlew publishToMavenLocal
``` ```

View File

@ -1,14 +1,10 @@
buildscript {
repositories {
google()
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.2")
}
}
plugins { plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("com.android.library").version("8.3.1").apply(false)
id("org.jetbrains.kotlin.android").version("1.9.23").apply(false)
id("org.gradle.maven-publish")
id("org.gradle.signing")
id("org.bitcoindevkit.plugins.generate-android-bindings").apply(false)
id("io.github.gradle-nexus.publish-plugin").version("1.1.0").apply(true)
} }
// library version is defined in gradle.properties // library version is defined in gradle.properties

View File

@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
kotlin.code.style=official kotlin.code.style=official
libraryVersion=1.0.0-alpha.2b-SNAPSHOT libraryVersion=1.0.0-alpha.12-SNAPSHOT

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

41
bdk-android/gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

20
bdk-android/justfile Normal file
View File

@ -0,0 +1,20 @@
default:
just --list
build:
./gradlew buildAndroidLib
clean:
rm -rf ../bdk-ffi/target/
rm -rf ./build/
rm -rf ./lib/build/
rm -rf ./plugins/build/
publish-local:
./gradlew publishToMavenLocal -P localBuild
test:
./gradlew connectedAndroidTest
test-specific TEST:
./gradlew test --tests {{TEST}}

View File

@ -5,25 +5,21 @@ val libraryVersion: String by project
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") version "1.6.10" id("org.jetbrains.kotlin.android")
id("maven-publish") id("org.gradle.maven-publish")
id("signing") id("org.gradle.signing")
// Custom plugin to generate the native libs and bindings file // Custom plugin to generate the native libs and bindings file
id("org.bitcoindevkit.plugins.generate-android-bindings") id("org.bitcoindevkit.plugins.generate-android-bindings")
} }
repositories {
mavenCentral()
google()
}
android { android {
compileSdk = 33 namespace = "org.bitcoindevkit"
compileSdk = 34
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 24
targetSdk = 33 targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
} }
@ -43,6 +39,20 @@ android {
} }
} }
kotlin {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
dependencies { dependencies {
implementation("net.java.dev.jna:jna:5.14.0@aar") implementation("net.java.dev.jna:jna:5.14.0@aar")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
@ -80,6 +90,13 @@ afterEvaluate {
url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT")
} }
} }
developers {
developer {
id.set("bdkdevelopers")
name.set("Bitcoin Dev Kit Developers")
email.set("dev@bitcoindevkit.org")
}
}
scm { scm {
connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git")
developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git")
@ -89,9 +106,22 @@ afterEvaluate {
} }
} }
} }
// This is required because we must ensure the moveNativeAndroidLibs task is executed after
// the mergeReleaseJniLibFolders (hard requirement introduced by our upgrade to Gradle 8.7)
tasks.named("mergeReleaseJniLibFolders") {
dependsOn(":lib:moveNativeAndroidLibs")
}
tasks.named("mergeDebugJniLibFolders") {
dependsOn(":lib:moveNativeAndroidLibs")
}
} }
signing { signing {
if (project.hasProperty("localBuild")) {
isRequired = false
}
val signingKeyId: String? by project val signingKeyId: String? by project
val signingKey: String? by project val signingKey: String? by project
val signingPassword: String? by project val signingPassword: String? by project
@ -99,8 +129,7 @@ signing {
sign(publishing.publications) sign(publishing.publications)
} }
// This task dependency ensures that we build the bindings // This task dependency ensures that we build the bindings binaries before running the tests
// binaries before running the tests
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
dependsOn("buildAndroidLib") dependsOn("buildAndroidLib")
} }

View File

@ -8,10 +8,14 @@ import java.io.File
import kotlin.test.AfterTest import kotlin.test.AfterTest
import kotlin.test.assertTrue import kotlin.test.assertTrue
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LiveTxBuilderTest { class LiveTxBuilderTest {
private val persistenceFilePath = InstrumentationRegistry private val persistenceFilePath = InstrumentationRegistry.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence3.sqlite"
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db" private val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
private val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -23,19 +27,21 @@ class LiveTxBuilderTest {
@Test @Test
fun testTxBuilder() { fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(wallet, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2.0f)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@ -44,26 +50,28 @@ class LiveTxBuilderTest {
@Test @Test
fun complexTxBuilder() { fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET) val fullScanRequest: FullScanRequest = wallet.startFullScan()
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL) println("Balance: ${wallet.balance().total.toSat()}")
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) assert(wallet.balance().total.toSat() > 0uL) {
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.TESTNET) "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf( val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL), ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), 4200uL), ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
) )
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.setRecipients(allRecipients) .setRecipients(allRecipients)
.feeRate(FeeRate.fromSatPerVb(4.0f)) .feeRate(FeeRate.fromSatPerVb(4uL))
.changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN) .changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN)
.enableRbf() .enableRbf()
.finish(wallet) .finish(wallet)

View File

@ -8,10 +8,15 @@ import java.io.File
import kotlin.test.AfterTest import kotlin.test.AfterTest
import kotlin.test.assertTrue import kotlin.test.assertTrue
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LiveWalletTest { class LiveWalletTest {
private val persistenceFilePath = InstrumentationRegistry private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db" .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence2.sqlite"
private val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
private val changeDescriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -23,22 +28,24 @@ class LiveWalletTest {
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val esploraClient: EsploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(wallet, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
val balance: Balance = wallet.getBalance() val balance: Balance = wallet.balance()
println("Balance: $balance") println("Balance: $balance")
assert(wallet.getBalance().total > 0uL) assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
println("Transactions count: ${wallet.transactions().count()}") println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3) val transactions = wallet.transactions().take(3)
for (tx in transactions) { for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx) val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.txid()}") println("Transaction: ${tx.transaction.computeTxid()}")
println("Sent ${sentAndReceived.sent}") println("Sent ${sentAndReceived.sent}")
println("Received ${sentAndReceived.received}") println("Received ${sentAndReceived.received}")
} }
@ -46,24 +53,22 @@ class LiveWalletTest {
@Test @Test
fun testBroadcastTransaction() { fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(wallet, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
println("New address: ${wallet.getAddress(AddressIndex.New).address}")
assert(wallet.getBalance().total > 0uL) { assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again." "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
} }
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(4.0f)) .feeRate(FeeRate.fromSatPerVb(4uL))
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@ -73,13 +78,13 @@ class LiveWalletTest {
assertTrue(walletDidSign) assertTrue(walletDidSign)
val tx: Transaction = psbt.extractTx() val tx: Transaction = psbt.extractTx()
println("Txid is: ${tx.txid()}") println("Txid is: ${tx.computeTxid()}")
val txFee: ULong = wallet.calculateFee(tx) val txFee: Amount = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}") println("Tx fee is: ${txFee.toSat()}")
val feeRate: FeeRate = wallet.calculateFeeRate(tx) val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.asSatPerVb()} sat/vB") println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")
esploraClient.broadcast(tx) esploraClient.broadcast(tx)
} }

View File

@ -15,7 +15,7 @@ class OfflineDescriptorTest {
assertEquals( assertEquals(
expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx",
actual = descriptor.asString() actual = descriptor.toString()
) )
} }
} }

View File

@ -13,7 +13,9 @@ import kotlin.test.AfterTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OfflineWalletTest { class OfflineWalletTest {
private val persistenceFilePath = InstrumentationRegistry private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db" .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence1.sqlite"
private val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
private val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -29,22 +31,17 @@ class OfflineWalletTest {
val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null)
val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET)
assertTrue(descriptor.asString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'") assertTrue(descriptor.toString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'")
} }
@Test @Test
fun testNewAddress() { fun testNewAddress() {
val descriptor: Descriptor = Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet(
descriptor, descriptor,
null, changeDescriptor,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network")
assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network")
@ -52,27 +49,22 @@ class OfflineWalletTest {
assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be")
assertEquals( assertEquals(
expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", expected = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
actual = addressInfo.address.asString() actual = addressInfo.address.toString()
) )
} }
@Test @Test
fun testBalance() { fun testBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet(
descriptor, descriptor,
null, changeDescriptor,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
assertEquals( assertEquals(
expected = 0uL, expected = 0uL,
actual = wallet.getBalance().total actual = wallet.balance().total.toSat()
) )
} }
} }

View File

@ -1,6 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="org.bitcoindevkit">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -1,6 +1,5 @@
package org.bitcoindevkit.plugins package org.bitcoindevkit.plugins
val operatingSystem: OS = when { val operatingSystem: OS = when {
System.getProperty("os.name").contains("mac", ignoreCase = true) -> OS.MAC System.getProperty("os.name").contains("mac", ignoreCase = true) -> OS.MAC
System.getProperty("os.name").contains("linux", ignoreCase = true) -> OS.LINUX System.getProperty("os.name").contains("linux", ignoreCase = true) -> OS.LINUX

View File

@ -17,6 +17,11 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
OS.OTHER -> throw Error("Cannot build Android library from current architecture") OS.OTHER -> throw Error("Cannot build Android library from current architecture")
} }
// if ANDROID_NDK_ROOT is not set, stop build
if (System.getenv("ANDROID_NDK_ROOT") == null) {
throw IllegalStateException("ANDROID_NDK_ROOT environment variable is not set; cannot build library")
}
// arm64-v8a is the most popular hardware architecture for Android // arm64-v8a is the most popular hardware architecture for Android
val buildAndroidAarch64Binary by tasks.register<Exec>("buildAndroidAarch64Binary") { val buildAndroidAarch64Binary by tasks.register<Exec>("buildAndroidAarch64Binary") {
@ -26,20 +31,13 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)
// if ANDROID_NDK_ROOT is not set then set it to github actions default
if (System.getenv("ANDROID_NDK_ROOT") == null) {
environment(
Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle")
)
}
environment( environment(
// add build toolchain to PATH // add build toolchain to PATH
Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"),
Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=21"), Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=24"),
Pair("AR", "llvm-ar"), Pair("AR", "llvm-ar"),
Pair("CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER", "aarch64-linux-android21-clang"), Pair("CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER", "aarch64-linux-android24-clang"),
Pair("CC", "aarch64-linux-android21-clang") Pair("CC", "aarch64-linux-android24-clang")
) )
doLast { doLast {
@ -56,20 +54,13 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)
// if ANDROID_NDK_ROOT is not set then set it to github actions default
if (System.getenv("ANDROID_NDK_ROOT") == null) {
environment(
Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle")
)
}
environment( environment(
// add build toolchain to PATH // add build toolchain to PATH
Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"),
Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=21"), Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=24"),
Pair("AR", "llvm-ar"), Pair("AR", "llvm-ar"),
Pair("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER", "x86_64-linux-android21-clang"), Pair("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER", "x86_64-linux-android24-clang"),
Pair("CC", "x86_64-linux-android21-clang") Pair("CC", "x86_64-linux-android24-clang")
) )
doLast { doLast {
@ -86,20 +77,13 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)
// if ANDROID_NDK_ROOT is not set then set it to github actions default
if (System.getenv("ANDROID_NDK_ROOT") == null) {
environment(
Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle")
)
}
environment( environment(
// add build toolchain to PATH // add build toolchain to PATH
Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), Pair("PATH", "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"),
Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=21"), Pair("CFLAGS", "-D__ANDROID_MIN_SDK_VERSION__=24"),
Pair("AR", "llvm-ar"), Pair("AR", "llvm-ar"),
Pair("CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER", "armv7a-linux-androideabi21-clang"), Pair("CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER", "armv7a-linux-androideabi24-clang"),
Pair("CC", "armv7a-linux-androideabi21-clang") Pair("CC", "armv7a-linux-androideabi24-clang")
) )
doLast { doLast {
@ -113,6 +97,8 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
val moveNativeAndroidLibs by tasks.register<Copy>("moveNativeAndroidLibs") { val moveNativeAndroidLibs by tasks.register<Copy>("moveNativeAndroidLibs") {
dependsOn(buildAndroidAarch64Binary) dependsOn(buildAndroidAarch64Binary)
dependsOn(buildAndroidArmv7Binary)
dependsOn(buildAndroidX86_64Binary)
into("${project.projectDir}/../lib/src/main/jniLibs/") into("${project.projectDir}/../lib/src/main/jniLibs/")
@ -137,14 +123,14 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
val generateAndroidBindings by tasks.register<Exec>("generateAndroidBindings") { val generateAndroidBindings by tasks.register<Exec>("generateAndroidBindings") {
dependsOn(moveNativeAndroidLibs) dependsOn(moveNativeAndroidLibs)
// val libraryPath = "${project.projectDir}/../../bdk-ffi/target/aarch64-linux-android/release-smaller/libbdkffi.so" val libraryPath = "${project.projectDir}/../../bdk-ffi/target/aarch64-linux-android/release-smaller/libbdkffi.so"
// workingDir("${project.projectDir}/../../bdk-ffi") workingDir("${project.projectDir}/../../bdk-ffi")
// val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "--library", libraryPath, "--language", "kotlin", "--out-dir", "../bdk-android/lib/src/main/kotlin", "--no-format") val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "--library", libraryPath, "--language", "kotlin", "--out-dir", "../bdk-android/lib/src/main/kotlin", "--no-format")
// The code above worked for uniffi 0.24.3 using the --library flag // The code above worked for uniffi 0.24.3 using the --library flag
// The code below works for uniffi 0.23.0 // The code below works for uniffi 0.23.0
workingDir("${project.projectDir}/../../bdk-ffi") // workingDir("${project.projectDir}/../../bdk-ffi")
val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "src/bdk.udl", "--language", "kotlin", "--config", "uniffi-android.toml", "--out-dir", "../bdk-android/lib/src/main/kotlin", "--no-format") // val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "src/bdk.udl", "--language", "kotlin", "--config", "uniffi-android.toml", "--out-dir", "../bdk-android/lib/src/main/kotlin", "--no-format")
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)

View File

@ -2,3 +2,17 @@ rootProject.name = "bdk-android"
include(":lib") include(":lib")
includeBuild("plugins") includeBuild("plugins")
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}

941
bdk-ffi/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bdk-ffi" name = "bdk-ffi"
version = "1.0.0-alpha.7" version = "1.0.0-alpha.11"
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk" repository = "https://github.com/bitcoindevkit/bdk"
edition = "2018" edition = "2018"
@ -18,19 +18,24 @@ path = "uniffi-bindgen.rs"
default = ["uniffi/cli"] default = ["uniffi/cli"]
[dependencies] [dependencies]
bdk = { version = "1.0.0-alpha.7", features = ["all-keys", "keys-bip39"] } bdk_wallet = { version = "1.0.0-alpha.13", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.9.0", default-features = false, features = ["std", "blocking"] } bdk_esplora = { version = "0.15.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
# bdk_esplora = { version = "0.9.0", default-features = false, features = ["std", "blocking", "async-https-rustls"] } # NOTE: This is a temporary workaround to use the electrum-client with the use-rustls-ring feature. It points to a fork
bdk_file_store = { version = "0.7.0" } # of bdk in which the bdk_electrum library uses the electrum-client with the use-rustls-ring feature.
bdk_electrum = { git = "https://github.com/thunderbiscuit/bdk/", package = "bdk_electrum", branch = "feature/electrum-client-ring-ffi-alpha13", default-features = false, features = ["use-rustls-ring"] }
# bdk_electrum = { version = "0.15.0" }
bdk_sqlite = { version = "0.2.0" }
bdk_bitcoind_rpc = { version = "0.12.0" }
bitcoin-internals = { version = "0.2.0", features = ["alloc"] }
uniffi = { version = "=0.26.1" } uniffi = { version = "=0.28.0" }
thiserror = "1.0.58" thiserror = "1.0.58"
[build-dependencies] [build-dependencies]
uniffi = { version = "=0.26.1", features = ["build"] } uniffi = { version = "=0.28.0", features = ["build"] }
[dev-dependencies] [dev-dependencies]
uniffi = { version = "=0.26.1", features = ["bindgen-tests"] } uniffi = { version = "=0.28.0", features = ["bindgen-tests"] }
assert_matches = "1.5.0" assert_matches = "1.5.0"
[profile.release-smaller] [profile.release-smaller]

12
bdk-ffi/justfile Normal file
View File

@ -0,0 +1,12 @@
default:
just --list
build:
cargo build
check:
cargo fmt
cargo clippy
test:
cargo test --lib

View File

@ -5,45 +5,277 @@ namespace bdk {};
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
[Error] [Error]
enum Alpha3Error { interface AddressParseError {
"Generic" Base58();
Bech32();
WitnessVersion(string error_message);
WitnessProgram(string error_message);
UnknownHrp();
LegacyAddressTooLong();
InvalidBase58PayloadLength();
InvalidLegacyPrefix();
NetworkValidation();
OtherAddressParseErr();
};
[Error]
interface Bip32Error {
CannotDeriveFromHardenedKey();
Secp256k1(string error_message);
InvalidChildNumber(u32 child_number);
InvalidChildNumberFormat();
InvalidDerivationPathFormat();
UnknownVersion(string version);
WrongExtendedKeyLength(u32 length);
Base58(string error_message);
Hex(string error_message);
InvalidPublicKeyHexLength(u32 length);
UnknownError(string error_message);
};
[Error]
interface Bip39Error {
BadWordCount(u64 word_count);
UnknownWord(u64 index);
BadEntropyBitCount(u64 bit_count);
InvalidChecksum();
AmbiguousLanguages(string languages);
}; };
[Error] [Error]
interface CalculateFeeError { interface CalculateFeeError {
MissingTxOut(sequence<OutPoint> out_points); MissingTxOut(sequence<OutPoint> out_points);
NegativeFee(i64 fee); NegativeFee(string amount);
}; };
[Error] [Error]
interface WalletCreationError { interface CannotConnectError {
Io(string e); Include(u32 height);
InvalidMagicBytes(sequence<u8> got, sequence<u8> expected); };
Descriptor();
Write(); [Error]
Load(); interface CreateTxError {
NotInitialized(); Descriptor(string error_message);
LoadedGenesisDoesNotMatch(); Policy(string error_message);
LoadedNetworkDoesNotMatch(Network expected, Network? got); SpendingPolicyRequired(string kind);
Version0();
Version1Csv();
LockTime(string requested, string required);
RbfSequence();
RbfSequenceCsv(string rbf, string csv);
FeeTooLow(string required);
FeeRateTooLow(string required);
NoUtxosSelected();
OutputBelowDustLimit(u64 index);
ChangePolicyDescriptor();
CoinSelection(string error_message);
InsufficientFunds(u64 needed, u64 available);
NoRecipients();
Psbt(string error_message);
MissingKeyOrigin(string key);
UnknownUtxo(string outpoint);
MissingNonWitnessUtxo(string outpoint);
MiniscriptPsbt(string error_message);
};
[Error]
interface DescriptorError {
InvalidHdKeyPath();
InvalidDescriptorChecksum();
HardenedDerivationXpub();
MultiPath();
Key(string error_message);
Policy(string error_message);
InvalidDescriptorCharacter(string char);
Bip32(string error_message);
Base58(string error_message);
Pk(string error_message);
Miniscript(string error_message);
Hex(string error_message);
ExternalAndInternalAreTheSame();
};
[Error]
interface DescriptorKeyError {
Parse(string error_message);
InvalidKeyType();
Bip32(string error_message);
};
[Error]
interface ElectrumError {
IOError(string error_message);
Json(string error_message);
Hex(string error_message);
Protocol(string error_message);
Bitcoin(string error_message);
AlreadySubscribed();
NotSubscribed();
InvalidResponse(string error_message);
Message(string error_message);
InvalidDNSNameError(string domain);
MissingDomain();
AllAttemptsErrored();
SharedIOError(string error_message);
CouldntLockReader();
Mpsc();
CouldNotCreateConnection(string error_message);
RequestAlreadyConsumed();
}; };
[Error] [Error]
interface EsploraError { interface EsploraError {
Ureq(string error_message); Minreq(string error_message);
UreqTransport(string error_message); HttpResponse(u16 status, string error_message);
Http(u16 status_code);
Io(string error_message);
NoHeader();
Parsing(string error_message); Parsing(string error_message);
StatusCode(string error_message);
BitcoinEncoding(string error_message); BitcoinEncoding(string error_message);
Hex(string error_message); HexToArray(string error_message);
HexToBytes(string error_message);
TransactionNotFound(); TransactionNotFound();
HeaderHeightNotFound(u32 height); HeaderHeightNotFound(u32 height);
HeaderHashNotFound(); HeaderHashNotFound();
InvalidHttpHeaderName(string name);
InvalidHttpHeaderValue(string value);
RequestAlreadyConsumed();
};
[Error]
interface ExtractTxError {
AbsurdFeeRate(u64 fee_rate);
MissingInputValue();
SendingTooMuch();
OtherExtractTxErr();
};
[Error]
enum FeeRateError {
"ArithmeticOverflow"
};
[Error]
interface FromScriptError {
UnrecognizedScript();
WitnessProgram(string error_message);
WitnessVersion(string error_message);
OtherFromScriptErr();
};
[Error]
interface ParseAmountError {
OutOfRange();
TooPrecise();
MissingDigits();
InputTooLarge();
InvalidCharacter(string error_message);
OtherParseAmountErr();
};
[Error]
interface PersistenceError {
Write(string error_message);
};
[Error]
interface PsbtError {
InvalidMagic();
MissingUtxo();
InvalidSeparator();
PsbtUtxoOutOfBounds();
InvalidKey(string key);
InvalidProprietaryKey();
DuplicateKey(string key);
UnsignedTxHasScriptSigs();
UnsignedTxHasScriptWitnesses();
MustHaveUnsignedTx();
NoMorePairs();
UnexpectedUnsignedTx();
NonStandardSighashType(u32 sighash);
InvalidHash(string hash);
InvalidPreimageHashPair();
CombineInconsistentKeySources(string xpub);
ConsensusEncoding(string encoding_error);
NegativeFee();
FeeOverflow();
InvalidPublicKey(string error_message);
InvalidSecp256k1PublicKey(string secp256k1_error);
InvalidXOnlyPublicKey();
InvalidEcdsaSignature(string error_message);
InvalidTaprootSignature(string error_message);
InvalidControlBlock();
InvalidLeafVersion();
Taproot();
TapTree(string error_message);
XPubKey();
Version(string error_message);
PartialDataConsumption();
Io(string error_message);
OtherPsbtErr();
};
[Error]
interface PsbtParseError {
PsbtEncoding(string error_message);
Base64Encoding(string error_message);
};
[Error]
interface InspectError {
RequestAlreadyConsumed();
};
[Error]
interface SignerError {
MissingKey();
InvalidKey();
UserCanceled();
InputIndexOutOfRange();
MissingNonWitnessUtxo();
InvalidNonWitnessUtxo();
MissingWitnessUtxo();
MissingWitnessScript();
MissingHdKeypath();
NonStandardSighash();
InvalidSighash();
SighashP2wpkh(string error_message);
SighashTaproot(string error_message);
TxInputsIndexError(string error_message);
MiniscriptPsbt(string error_message);
External(string error_message);
};
[Error]
interface SqliteError {
InvalidNetwork(Network expected, Network given);
Sqlite(string rusqlite_error);
};
[Error]
interface TransactionError {
Io();
OversizedVectorAllocation();
InvalidChecksum(string expected, string actual);
NonMinimalVarInt();
ParseFailed();
UnsupportedSegwitFlag(u8 flag);
OtherTransactionErr();
};
[Error]
interface TxidParseError {
InvalidTxid(string txid);
};
[Error]
interface WalletCreationError {
Descriptor(string error_message);
LoadedGenesisDoesNotMatch(string expected, string got);
LoadedNetworkDoesNotMatch(Network expected, Network? got);
LoadedDescriptorDoesNotMatch(string got, KeychainKind keychain);
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - types module // bdk_wallet crate - types module
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
enum KeychainKind { enum KeychainKind {
@ -57,25 +289,18 @@ dictionary AddressInfo {
KeychainKind keychain; KeychainKind keychain;
}; };
[Enum]
interface AddressIndex {
New();
LastUnused();
Peek(u32 index);
};
dictionary Balance { dictionary Balance {
u64 immature; Amount immature;
u64 trusted_pending; Amount trusted_pending;
u64 untrusted_pending; Amount untrusted_pending;
u64 confirmed; Amount confirmed;
u64 trusted_spendable; Amount trusted_spendable;
u64 total; Amount total;
}; };
dictionary LocalOutput { dictionary LocalOutput {
@ -90,22 +315,43 @@ dictionary TxOut {
Script script_pubkey; Script script_pubkey;
}; };
// ------------------------------------------------------------------------ [Enum]
// bdk crate - wallet module interface ChainPosition {
// ------------------------------------------------------------------------ Confirmed(u32 height, u64 timestamp);
Unconfirmed(u64 timestamp);
interface FeeRate {
[Name=from_sat_per_vb]
constructor(f32 sat_per_vb);
[Name=from_sat_per_kwu]
constructor(f32 sat_per_kwu);
f32 as_sat_per_vb();
f32 sat_per_kwu();
}; };
dictionary CanonicalTx {
Transaction transaction;
ChainPosition chain_position;
};
interface FullScanRequest {
[Throws=InspectError]
FullScanRequest inspect_spks_for_all_keychains(FullScanScriptInspector inspector);
};
interface SyncRequest {
[Throws=InspectError]
SyncRequest inspect_spks(SyncScriptInspector inspector);
};
[Trait, WithForeign]
interface SyncScriptInspector {
void inspect(Script script, u64 total);
};
[Trait, WithForeign]
interface FullScanScriptInspector {
void inspect(KeychainKind keychain, u32 index, Script script);
};
interface ChangeSet {};
// ------------------------------------------------------------------------
// bdk_wallet crate - wallet module
// ------------------------------------------------------------------------
enum ChangeSpendPolicy { enum ChangeSpendPolicy {
"ChangeAllowed", "ChangeAllowed",
"OnlyChange", "OnlyChange",
@ -114,34 +360,47 @@ enum ChangeSpendPolicy {
interface Wallet { interface Wallet {
[Throws=WalletCreationError] [Throws=WalletCreationError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network); constructor(Descriptor descriptor, Descriptor change_descriptor, Network network);
AddressInfo get_address(AddressIndex address_index); [Name=new_or_load, Throws=WalletCreationError]
constructor(Descriptor descriptor, Descriptor change_descriptor, ChangeSet? change_set, Network network);
[Throws=Alpha3Error] AddressInfo reveal_next_address(KeychainKind keychain);
AddressInfo try_get_internal_address(AddressIndex address_index);
Network network(); Network network();
Balance get_balance(); Balance balance();
[Throws=Alpha3Error] [Throws=CannotConnectError]
void apply_update(Update update); void apply_update(Update update);
boolean is_mine([ByRef] Script script); boolean is_mine([ByRef] Script script);
[Throws=Alpha3Error] [Throws=SignerError]
boolean sign(PartiallySignedTransaction psbt); boolean sign(Psbt psbt);
SentAndReceivedValues sent_and_received([ByRef] Transaction tx); SentAndReceivedValues sent_and_received([ByRef] Transaction tx);
sequence<Transaction> transactions(); sequence<CanonicalTx> transactions();
[Throws=TxidParseError]
CanonicalTx? get_tx(string txid);
[Throws=CalculateFeeError] [Throws=CalculateFeeError]
u64 calculate_fee([ByRef] Transaction tx); Amount calculate_fee([ByRef] Transaction tx);
[Throws=CalculateFeeError] [Throws=CalculateFeeError]
FeeRate calculate_fee_rate([ByRef] Transaction tx); FeeRate calculate_fee_rate([ByRef] Transaction tx);
sequence<LocalOutput> list_unspent();
sequence<LocalOutput> list_output();
FullScanRequest start_full_scan();
SyncRequest start_sync_with_revealed_spks();
ChangeSet? take_staged();
}; };
interface Update {}; interface Update {};
@ -149,7 +408,7 @@ interface Update {};
interface TxBuilder { interface TxBuilder {
constructor(); constructor();
TxBuilder add_recipient([ByRef] Script script, u64 amount); TxBuilder add_recipient([ByRef] Script script, Amount amount);
TxBuilder set_recipients(sequence<ScriptAmount> recipients); TxBuilder set_recipients(sequence<ScriptAmount> recipients);
@ -169,7 +428,7 @@ interface TxBuilder {
TxBuilder fee_rate([ByRef] FeeRate fee_rate); TxBuilder fee_rate([ByRef] FeeRate fee_rate);
TxBuilder fee_absolute(u64 fee); TxBuilder fee_absolute(Amount fee);
TxBuilder drain_wallet(); TxBuilder drain_wallet();
@ -179,54 +438,66 @@ interface TxBuilder {
TxBuilder enable_rbf_with_sequence(u32 nsequence); TxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=Alpha3Error] [Throws=CreateTxError]
PartiallySignedTransaction finish([ByRef] Wallet wallet); Psbt finish([ByRef] Wallet wallet);
}; };
interface BumpFeeTxBuilder { interface BumpFeeTxBuilder {
constructor(string txid, f32 fee_rate); constructor(string txid, FeeRate fee_rate);
BumpFeeTxBuilder allow_shrinking(Script script_pubkey);
BumpFeeTxBuilder enable_rbf(); BumpFeeTxBuilder enable_rbf();
BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence); BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=Alpha3Error] [Throws=CreateTxError]
PartiallySignedTransaction finish([ByRef] Wallet wallet); Psbt finish([ByRef] Wallet wallet);
};
// ------------------------------------------------------------------------
// bdk_sqlite crate
// ------------------------------------------------------------------------
interface SqliteStore {
[Throws=SqliteError]
constructor(string path);
[Throws=SqliteError]
void write([ByRef] ChangeSet change_set);
[Throws=SqliteError]
ChangeSet? read();
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - descriptor module // bdk crate - descriptor module
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
[Traits=(Display)]
interface Mnemonic { interface Mnemonic {
constructor(WordCount word_count); constructor(WordCount word_count);
[Name=from_string, Throws=Alpha3Error] [Name=from_string, Throws=Bip39Error]
constructor(string mnemonic); constructor(string mnemonic);
[Name=from_entropy, Throws=Alpha3Error] [Name=from_entropy, Throws=Bip39Error]
constructor(sequence<u8> entropy); constructor(sequence<u8> entropy);
string as_string();
}; };
interface DerivationPath { interface DerivationPath {
[Throws=Alpha3Error] [Throws=Bip32Error]
constructor(string path); constructor(string path);
}; };
interface DescriptorSecretKey { interface DescriptorSecretKey {
constructor(Network network, [ByRef] Mnemonic mnemonic, string? password); constructor(Network network, [ByRef] Mnemonic mnemonic, string? password);
[Name=from_string, Throws=Alpha3Error] [Name=from_string, Throws=DescriptorKeyError]
constructor(string secret_key); constructor(string secret_key);
[Throws=Alpha3Error] [Throws=DescriptorKeyError]
DescriptorSecretKey derive([ByRef] DerivationPath path); DescriptorSecretKey derive([ByRef] DerivationPath path);
[Throws=Alpha3Error] [Throws=DescriptorKeyError]
DescriptorSecretKey extend([ByRef] DerivationPath path); DescriptorSecretKey extend([ByRef] DerivationPath path);
DescriptorPublicKey as_public(); DescriptorPublicKey as_public();
@ -237,20 +508,21 @@ interface DescriptorSecretKey {
}; };
interface DescriptorPublicKey { interface DescriptorPublicKey {
[Name=from_string, Throws=Alpha3Error] [Name=from_string, Throws=DescriptorKeyError]
constructor(string public_key); constructor(string public_key);
[Throws=Alpha3Error] [Throws=DescriptorKeyError]
DescriptorPublicKey derive([ByRef] DerivationPath path); DescriptorPublicKey derive([ByRef] DerivationPath path);
[Throws=Alpha3Error] [Throws=DescriptorKeyError]
DescriptorPublicKey extend([ByRef] DerivationPath path); DescriptorPublicKey extend([ByRef] DerivationPath path);
string as_string(); string as_string();
}; };
[Traits=(Display)]
interface Descriptor { interface Descriptor {
[Throws=Alpha3Error] [Throws=DescriptorError]
constructor(string descriptor, Network network); constructor(string descriptor, Network network);
[Name=new_bip44] [Name=new_bip44]
@ -277,9 +549,7 @@ interface Descriptor {
[Name=new_bip86_public] [Name=new_bip86_public]
constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network);
string as_string(); string to_string_with_secret();
string as_string_private();
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -290,28 +560,49 @@ interface EsploraClient {
constructor(string url); constructor(string url);
[Throws=EsploraError] [Throws=EsploraError]
Update full_scan(Wallet wallet, u64 stop_gap, u64 parallel_requests); Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 parallel_requests);
[Throws=Alpha3Error] [Throws=EsploraError]
Update sync(SyncRequest sync_request, u64 parallel_requests);
[Throws=EsploraError]
void broadcast([ByRef] Transaction transaction); void broadcast([ByRef] Transaction transaction);
}; };
// ------------------------------------------------------------------------
// bdk_electrum crate
// ------------------------------------------------------------------------
interface ElectrumClient {
[Throws=ElectrumError]
constructor(string url);
[Throws=ElectrumError]
Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 batch_size, boolean fetch_prev_txouts);
[Throws=ElectrumError]
Update sync(SyncRequest sync_request, u64 batch_size, boolean fetch_prev_txouts);
[Throws=ElectrumError]
string broadcast([ByRef] Transaction transaction);
};
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk-ffi-defined types // bdk-ffi-defined types
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
dictionary ScriptAmount { dictionary ScriptAmount {
Script script; Script script;
u64 amount; Amount amount;
}; };
dictionary SentAndReceivedValues { dictionary SentAndReceivedValues {
u64 sent; Amount sent;
u64 received; Amount received;
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - bitcoin re-exports // bdk_wallet crate - bitcoin re-exports
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
interface Script { interface Script {
@ -336,50 +627,102 @@ enum WordCount {
"Words24", "Words24",
}; };
[Traits=(Display)]
interface Address { interface Address {
[Throws=Alpha3Error] [Throws=AddressParseError]
constructor(string address, Network network); constructor(string address, Network network);
Network network(); [Name=from_script, Throws=FromScriptError]
constructor(Script script, Network network);
Script script_pubkey(); Script script_pubkey();
string to_qr_uri(); string to_qr_uri();
string as_string();
boolean is_valid_for_network(Network network); boolean is_valid_for_network(Network network);
}; };
interface Transaction { interface Transaction {
[Throws=Alpha3Error] [Throws=TransactionError]
constructor(sequence<u8> transaction_bytes); constructor(sequence<u8> transaction_bytes);
string txid(); string compute_txid();
u64 size(); u64 total_size();
u64 vsize(); u64 vsize();
boolean is_coin_base(); boolean is_coinbase();
boolean is_explicitly_rbf(); boolean is_explicitly_rbf();
boolean is_lock_time_enabled(); boolean is_lock_time_enabled();
i32 version(); i32 version();
sequence<u8> serialize();
u64 weight();
sequence<TxIn> input();
sequence<TxOut> output();
u32 lock_time();
}; };
interface PartiallySignedTransaction { interface Psbt {
[Throws=Alpha3Error] [Throws=PsbtParseError]
constructor(string psbt_base64); constructor(string psbt_base64);
string serialize(); string serialize();
[Throws=ExtractTxError]
Transaction extract_tx(); Transaction extract_tx();
[Throws=PsbtError]
u64 fee();
[Throws=PsbtError]
Psbt combine(Psbt other);
string json_serialize();
}; };
dictionary OutPoint { dictionary OutPoint {
string txid; string txid;
u32 vout; u32 vout;
}; };
interface Amount {
[Name=from_sat]
constructor(u64 from_sat);
[Name=from_btc, Throws=ParseAmountError]
constructor(f64 from_btc);
u64 to_sat();
f64 to_btc();
};
interface FeeRate {
[Name=from_sat_per_vb, Throws=FeeRateError]
constructor(u64 sat_per_vb);
[Name=from_sat_per_kwu]
constructor(u64 sat_per_kwu);
u64 to_sat_per_vb_ceil();
u64 to_sat_per_vb_floor();
u64 to_sat_per_kwu();
};
dictionary TxIn {
OutPoint previous_output;
Script script_sig;
u32 sequence;
sequence<sequence<u8>> witness;
};

View File

@ -1,19 +1,65 @@
use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked}; use crate::error::{
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; AddressParseError, FeeRateError, FromScriptError, PsbtError, PsbtParseError, TransactionError,
use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut; };
use bdk::bitcoin::consensus::Decodable;
use bdk::bitcoin::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction;
use bdk::bitcoin::Address as BdkAddress;
use bdk::bitcoin::Network;
use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::bitcoin::Txid;
use crate::error::Alpha3Error; use bdk_bitcoind_rpc::bitcoincore_rpc::jsonrpc::serde_json;
use std::io::Cursor; use bdk_wallet::bitcoin::address::{NetworkChecked, NetworkUnchecked};
use bdk_wallet::bitcoin::amount::ParseAmountError;
use bdk_wallet::bitcoin::consensus::encode::serialize;
use bdk_wallet::bitcoin::consensus::Decodable;
use bdk_wallet::bitcoin::io::Cursor;
use bdk_wallet::bitcoin::psbt::ExtractTxError;
use bdk_wallet::bitcoin::Address as BdkAddress;
use bdk_wallet::bitcoin::Amount as BdkAmount;
use bdk_wallet::bitcoin::FeeRate as BdkFeeRate;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::OutPoint as BdkOutPoint;
use bdk_wallet::bitcoin::Psbt as BdkPsbt;
use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf;
use bdk_wallet::bitcoin::Transaction as BdkTransaction;
use bdk_wallet::bitcoin::TxIn as BdkTxIn;
use bdk_wallet::bitcoin::TxOut as BdkTxOut;
use bdk_wallet::bitcoin::Txid;
use std::fmt::Display;
use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Amount(pub(crate) BdkAmount);
impl Amount {
pub fn from_sat(sat: u64) -> Self {
Amount(BdkAmount::from_sat(sat))
}
pub fn from_btc(btc: f64) -> Result<Self, ParseAmountError> {
let bdk_amount = BdkAmount::from_btc(btc).map_err(ParseAmountError::from)?;
Ok(Amount(bdk_amount))
}
pub fn to_sat(&self) -> u64 {
self.0.to_sat()
}
pub fn to_btc(&self) -> f64 {
self.0.to_btc()
}
}
impl From<Amount> for BdkAmount {
fn from(amount: Amount) -> Self {
amount.0
}
}
impl From<BdkAmount> for Amount {
fn from(amount: BdkAmount) -> Self {
Amount(amount)
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Script(pub(crate) BdkScriptBuf); pub struct Script(pub(crate) BdkScriptBuf);
@ -35,65 +81,32 @@ impl From<BdkScriptBuf> for Script {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct Address { pub struct Address(BdkAddress<NetworkChecked>);
inner: BdkAddress<NetworkChecked>,
}
impl Address { impl Address {
pub fn new(address: String, network: Network) -> Result<Self, Alpha3Error> { pub fn new(address: String, network: Network) -> Result<Self, AddressParseError> {
let parsed_address = address let parsed_address = address.parse::<bdk_wallet::bitcoin::Address<NetworkUnchecked>>()?;
.parse::<bdk::bitcoin::Address<NetworkUnchecked>>() let network_checked_address = parsed_address.require_network(network)?;
.map_err(|_| Alpha3Error::Generic)?;
let network_checked_address = parsed_address Ok(Address(network_checked_address))
.require_network(network)
.map_err(|_| Alpha3Error::Generic)?;
Ok(Address {
inner: network_checked_address,
})
} }
/// alternative constructor pub fn from_script(script: Arc<Script>, network: Network) -> Result<Self, FromScriptError> {
// fn from_script(script: Arc<Script>, network: Network) -> Result<Self, BdkError> { let address = BdkAddress::from_script(&script.0.clone(), network)?;
// BdkAddress::from_script(&script.inner, network)
// .map(|a| Address { inner: a })
// .map_err(|e| BdkError::Generic(e.to_string()))
// }
//
// fn payload(&self) -> Payload {
// match &self.inner.payload.clone() {
// BdkPayload::PubkeyHash(pubkey_hash) => Payload::PubkeyHash {
// pubkey_hash: pubkey_hash.to_vec(),
// },
// BdkPayload::ScriptHash(script_hash) => Payload::ScriptHash {
// script_hash: script_hash.to_vec(),
// },
// BdkPayload::WitnessProgram { version, program } => Payload::WitnessProgram {
// version: *version,
// program: program.clone(),
// },
// }
// }
pub fn network(&self) -> Network { Ok(Address(address))
self.inner.network
} }
pub fn script_pubkey(&self) -> Arc<Script> { pub fn script_pubkey(&self) -> Arc<Script> {
Arc::new(Script(self.inner.script_pubkey())) Arc::new(Script(self.0.script_pubkey()))
} }
pub fn to_qr_uri(&self) -> String { pub fn to_qr_uri(&self) -> String {
self.inner.to_qr_uri() self.0.to_qr_uri()
}
pub fn as_string(&self) -> String {
self.inner.to_string()
} }
pub fn is_valid_for_network(&self, network: Network) -> bool { pub fn is_valid_for_network(&self, network: Network) -> bool {
let address_str = self.inner.to_string(); let address_str = self.0.to_string();
if let Ok(unchecked_address) = address_str.parse::<BdkAddress<NetworkUnchecked>>() { if let Ok(unchecked_address) = address_str.parse::<BdkAddress<NetworkUnchecked>>() {
unchecked_address.is_valid_for_network(network) unchecked_address.is_valid_for_network(network)
} else { } else {
@ -102,171 +115,145 @@ impl Address {
} }
} }
impl Display for Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Address> for BdkAddress { impl From<Address> for BdkAddress {
fn from(address: Address) -> Self { fn from(address: Address) -> Self {
address.inner address.0
} }
} }
impl From<BdkAddress> for Address { impl From<BdkAddress> for Address {
fn from(address: BdkAddress) -> Self { fn from(address: BdkAddress) -> Self {
Address { inner: address } Address(address)
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transaction { pub struct Transaction(BdkTransaction);
inner: BdkTransaction,
}
impl Transaction { impl Transaction {
pub fn new(transaction_bytes: Vec<u8>) -> Result<Self, Alpha3Error> { pub fn new(transaction_bytes: Vec<u8>) -> Result<Self, TransactionError> {
let mut decoder = Cursor::new(transaction_bytes); let mut decoder = Cursor::new(transaction_bytes);
let tx: BdkTransaction = let tx: BdkTransaction = BdkTransaction::consensus_decode(&mut decoder)?;
BdkTransaction::consensus_decode(&mut decoder).map_err(|_| Alpha3Error::Generic)?; Ok(Transaction(tx))
Ok(Transaction { inner: tx })
} }
pub fn txid(&self) -> String { pub fn compute_txid(&self) -> String {
self.inner.txid().to_string() self.0.compute_txid().to_string()
} }
// fn weight(&self) -> u64 { pub fn weight(&self) -> u64 {
// self.inner.weight() as u64 self.0.weight().to_wu()
// } }
pub fn size(&self) -> u64 { pub fn total_size(&self) -> u64 {
self.inner.size() as u64 self.0.total_size() as u64
} }
pub fn vsize(&self) -> u64 { pub fn vsize(&self) -> u64 {
self.inner.vsize() as u64 self.0.vsize() as u64
} }
// fn serialize(&self) -> Vec<u8> { pub fn is_coinbase(&self) -> bool {
// self.inner.serialize() self.0.is_coinbase()
// }
pub fn is_coin_base(&self) -> bool {
self.inner.is_coin_base()
} }
pub fn is_explicitly_rbf(&self) -> bool { pub fn is_explicitly_rbf(&self) -> bool {
self.inner.is_explicitly_rbf() self.0.is_explicitly_rbf()
} }
pub fn is_lock_time_enabled(&self) -> bool { pub fn is_lock_time_enabled(&self) -> bool {
self.inner.is_lock_time_enabled() self.0.is_lock_time_enabled()
} }
pub fn version(&self) -> i32 { pub fn version(&self) -> i32 {
self.inner.version self.0.version.0
} }
// fn lock_time(&self) -> u32 { pub fn serialize(&self) -> Vec<u8> {
// self.inner.lock_time.0 serialize(&self.0)
// } }
// fn input(&self) -> Vec<TxIn> { pub fn input(&self) -> Vec<TxIn> {
// self.inner.input.iter().map(|x| x.into()).collect() self.0.input.iter().map(|tx_in| tx_in.into()).collect()
// } }
//
// fn output(&self) -> Vec<TxOut> { pub fn output(&self) -> Vec<TxOut> {
// self.inner.output.iter().map(|x| x.into()).collect() self.0.output.iter().map(|tx_out| tx_out.into()).collect()
// } }
pub fn lock_time(&self) -> u32 {
self.0.lock_time.to_consensus_u32()
}
} }
impl From<BdkTransaction> for Transaction { impl From<BdkTransaction> for Transaction {
fn from(tx: BdkTransaction) -> Self { fn from(tx: BdkTransaction) -> Self {
Transaction { inner: tx } Transaction(tx)
} }
} }
impl From<&BdkTransaction> for Transaction { impl From<&BdkTransaction> for Transaction {
fn from(tx: &BdkTransaction) -> Self { fn from(tx: &BdkTransaction) -> Self {
Transaction { inner: tx.clone() } Transaction(tx.clone())
} }
} }
impl From<&Transaction> for BdkTransaction { impl From<&Transaction> for BdkTransaction {
fn from(tx: &Transaction) -> Self { fn from(tx: &Transaction) -> Self {
tx.inner.clone() tx.0.clone()
} }
} }
pub struct PartiallySignedTransaction { pub struct Psbt(pub(crate) Mutex<BdkPsbt>);
pub(crate) inner: Mutex<BdkPartiallySignedTransaction>,
}
impl PartiallySignedTransaction { impl Psbt {
pub(crate) fn new(psbt_base64: String) -> Result<Self, Alpha3Error> { pub(crate) fn new(psbt_base64: String) -> Result<Self, PsbtParseError> {
let psbt: BdkPartiallySignedTransaction = let psbt: BdkPsbt = BdkPsbt::from_str(&psbt_base64)?;
BdkPartiallySignedTransaction::from_str(&psbt_base64) Ok(Psbt(Mutex::new(psbt)))
.map_err(|_| Alpha3Error::Generic)?;
Ok(PartiallySignedTransaction {
inner: Mutex::new(psbt),
})
} }
pub(crate) fn serialize(&self) -> String { pub(crate) fn serialize(&self) -> String {
let psbt = self.inner.lock().unwrap().clone(); let psbt = self.0.lock().unwrap().clone();
psbt.to_string() psbt.to_string()
} }
// pub(crate) fn txid(&self) -> String { pub(crate) fn extract_tx(&self) -> Result<Arc<Transaction>, ExtractTxError> {
// let tx = self.inner.lock().unwrap().clone().extract_tx(); let tx: BdkTransaction = self.0.lock().unwrap().clone().extract_tx()?;
// let txid = tx.txid(); let transaction: Transaction = tx.into();
// txid.to_hex() Ok(Arc::new(transaction))
// }
pub(crate) fn extract_tx(&self) -> Arc<Transaction> {
let tx = self.inner.lock().unwrap().clone().extract_tx();
Arc::new(tx.into())
} }
// /// Combines this PartiallySignedTransaction with other PSBT as described by BIP 174. pub(crate) fn fee(&self) -> Result<u64, PsbtError> {
// /// self.0
// /// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)` .lock()
// pub(crate) fn combine( .unwrap()
// &self, .fee()
// other: Arc<PartiallySignedTransaction>, .map(|fee| fee.to_sat())
// ) -> Result<Arc<PartiallySignedTransaction>, BdkError> { .map_err(PsbtError::from)
// let other_psbt = other.inner.lock().unwrap().clone(); }
// let mut original_psbt = self.inner.lock().unwrap().clone();
//
// original_psbt.combine(other_psbt)?;
// Ok(Arc::new(PartiallySignedTransaction {
// inner: Mutex::new(original_psbt),
// }))
// }
// /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats. pub(crate) fn combine(&self, other: Arc<Psbt>) -> Result<Arc<Psbt>, PsbtError> {
// /// If the PSBT is missing a TxOut for an input returns None. let mut original_psbt = self.0.lock().unwrap().clone();
// pub(crate) fn fee_amount(&self) -> Option<u64> { let other_psbt = other.0.lock().unwrap().clone();
// self.inner.lock().unwrap().fee_amount() original_psbt.combine(other_psbt)?;
// } Ok(Arc::new(Psbt(Mutex::new(original_psbt))))
}
// /// The transaction's fee rate. This value will only be accurate if calculated AFTER the pub(crate) fn json_serialize(&self) -> String {
// /// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the let psbt = self.0.lock().unwrap();
// /// transaction. serde_json::to_string(psbt.deref()).unwrap()
// /// If the PSBT is missing a TxOut for an input returns None. }
// pub(crate) fn fee_rate(&self) -> Option<Arc<FeeRate>> {
// self.inner.lock().unwrap().fee_rate().map(Arc::new)
// }
// /// Serialize the PSBT data structure as a String of JSON.
// pub(crate) fn json_serialize(&self) -> String {
// let psbt = self.inner.lock().unwrap();
// serde_json::to_string(psbt.deref()).unwrap()
// }
} }
impl From<BdkPartiallySignedTransaction> for PartiallySignedTransaction { impl From<BdkPsbt> for Psbt {
fn from(psbt: BdkPartiallySignedTransaction) -> Self { fn from(psbt: BdkPsbt) -> Self {
PartiallySignedTransaction { Psbt(Mutex::new(psbt))
inner: Mutex::new(psbt),
}
} }
} }
@ -294,6 +281,28 @@ impl From<&BdkOutPoint> for OutPoint {
} }
} }
#[derive(Debug, Clone)]
pub struct TxIn {
pub previous_output: OutPoint,
pub script_sig: Arc<Script>,
pub sequence: u32,
pub witness: Vec<Vec<u8>>,
}
impl From<&BdkTxIn> for TxIn {
fn from(tx_in: &BdkTxIn) -> Self {
TxIn {
previous_output: OutPoint {
txid: tx_in.previous_output.txid.to_string(),
vout: tx_in.previous_output.vout,
},
script_sig: Arc::new(Script(tx_in.script_sig.clone())),
sequence: tx_in.sequence.0,
witness: tx_in.witness.to_vec(),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TxOut { pub struct TxOut {
pub value: u64, pub value: u64,
@ -303,12 +312,41 @@ pub struct TxOut {
impl From<&BdkTxOut> for TxOut { impl From<&BdkTxOut> for TxOut {
fn from(tx_out: &BdkTxOut) -> Self { fn from(tx_out: &BdkTxOut) -> Self {
TxOut { TxOut {
value: tx_out.value, value: tx_out.value.to_sat(),
script_pubkey: Arc::new(Script(tx_out.script_pubkey.clone())), script_pubkey: Arc::new(Script(tx_out.script_pubkey.clone())),
} }
} }
} }
#[derive(Clone, Debug)]
pub struct FeeRate(pub(crate) BdkFeeRate);
impl FeeRate {
pub fn from_sat_per_vb(sat_per_vb: u64) -> Result<Self, FeeRateError> {
let fee_rate: Option<BdkFeeRate> = BdkFeeRate::from_sat_per_vb(sat_per_vb);
match fee_rate {
Some(fee_rate) => Ok(FeeRate(fee_rate)),
None => Err(FeeRateError::ArithmeticOverflow),
}
}
pub fn from_sat_per_kwu(sat_per_kwu: u64) -> Self {
FeeRate(BdkFeeRate::from_sat_per_kwu(sat_per_kwu))
}
pub fn to_sat_per_vb_ceil(&self) -> u64 {
self.0.to_sat_per_vb_ceil()
}
pub fn to_sat_per_vb_floor(&self) -> u64 {
self.0.to_sat_per_vb_floor()
}
pub fn to_sat_per_kwu(&self) -> u64 {
self.0.to_sat_per_kwu()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::bitcoin::Address; use crate::bitcoin::Address;
@ -334,11 +372,6 @@ mod tests {
docs_address_testnet.is_valid_for_network(Network::Regtest), docs_address_testnet.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest" "Address should be valid for Regtest"
); );
assert_ne!(
docs_address_testnet.network(),
Network::Bitcoin,
"Address should not be parsed as Bitcoin"
);
let docs_address_mainnet_str = "32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf"; let docs_address_mainnet_str = "32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf";
let docs_address_mainnet = let docs_address_mainnet =
@ -347,21 +380,6 @@ mod tests {
docs_address_mainnet.is_valid_for_network(Network::Bitcoin), docs_address_mainnet.is_valid_for_network(Network::Bitcoin),
"Address should be valid for Bitcoin" "Address should be valid for Bitcoin"
); );
assert_ne!(
docs_address_mainnet.network(),
Network::Testnet,
"Address should not be valid for Testnet"
);
assert_ne!(
docs_address_mainnet.network(),
Network::Signet,
"Address should not be valid for Signet"
);
assert_ne!(
docs_address_mainnet.network(),
Network::Regtest,
"Address should not be valid for Regtest"
);
// ====Bech32==== // ====Bech32====

View File

@ -1,18 +1,19 @@
use crate::error::Alpha3Error; use crate::error::DescriptorError;
use crate::keys::DescriptorPublicKey; use crate::keys::DescriptorPublicKey;
use crate::keys::DescriptorSecretKey; use crate::keys::DescriptorSecretKey;
use std::fmt::Display;
use bdk::bitcoin::bip32::Fingerprint; use bdk_wallet::bitcoin::bip32::Fingerprint;
use bdk::bitcoin::key::Secp256k1; use bdk_wallet::bitcoin::key::Secp256k1;
use bdk::bitcoin::Network; use bdk_wallet::bitcoin::Network;
use bdk::descriptor::{ExtendedDescriptor, IntoWalletDescriptor}; use bdk_wallet::descriptor::{ExtendedDescriptor, IntoWalletDescriptor};
use bdk::keys::DescriptorPublicKey as BdkDescriptorPublicKey; use bdk_wallet::keys::DescriptorPublicKey as BdkDescriptorPublicKey;
use bdk::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap}; use bdk_wallet::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap};
use bdk::template::{ use bdk_wallet::template::{
Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public, Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public,
DescriptorTemplate, DescriptorTemplate,
}; };
use bdk::KeychainKind; use bdk_wallet::KeychainKind;
use std::str::FromStr; use std::str::FromStr;
@ -23,7 +24,7 @@ pub struct Descriptor {
} }
impl Descriptor { impl Descriptor {
pub(crate) fn new(descriptor: String, network: Network) -> Result<Self, Alpha3Error> { pub(crate) fn new(descriptor: String, network: Network) -> Result<Self, DescriptorError> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let (extended_descriptor, key_map) = descriptor.into_wallet_descriptor(&secp, network)?; let (extended_descriptor, key_map) = descriptor.into_wallet_descriptor(&secp, network)?;
Ok(Self { Ok(Self {
@ -37,7 +38,7 @@ impl Descriptor {
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.inner; let derivable_key = &secret_key.0;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@ -65,7 +66,7 @@ impl Descriptor {
network: Network, network: Network,
) -> Self { ) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap(); let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.inner; let derivable_key = &public_key.0;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@ -94,7 +95,7 @@ impl Descriptor {
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.inner; let derivable_key = &secret_key.0;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@ -122,7 +123,7 @@ impl Descriptor {
network: Network, network: Network,
) -> Self { ) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap(); let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.inner; let derivable_key = &public_key.0;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@ -151,7 +152,7 @@ impl Descriptor {
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.inner; let derivable_key = &secret_key.0;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@ -179,7 +180,7 @@ impl Descriptor {
network: Network, network: Network,
) -> Self { ) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap(); let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.inner; let derivable_key = &public_key.0;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@ -208,7 +209,7 @@ impl Descriptor {
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.inner; let derivable_key = &secret_key.0;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@ -236,7 +237,7 @@ impl Descriptor {
network: Network, network: Network,
) -> Self { ) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap(); let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.inner; let derivable_key = &public_key.0;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@ -260,14 +261,16 @@ impl Descriptor {
} }
} }
pub(crate) fn as_string_private(&self) -> String { pub(crate) fn to_string_with_secret(&self) -> String {
let descriptor = &self.extended_descriptor; let descriptor = &self.extended_descriptor;
let key_map = &self.key_map; let key_map = &self.key_map;
descriptor.to_string_with_secret(key_map) descriptor.to_string_with_secret(key_map)
} }
}
pub(crate) fn as_string(&self) -> String { impl Display for Descriptor {
self.extended_descriptor.to_string() fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.extended_descriptor)
} }
} }
@ -275,8 +278,8 @@ impl Descriptor {
mod test { mod test {
use crate::*; use crate::*;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use bdk_wallet::bitcoin::Network;
use crate::Alpha3Error; use bdk_wallet::KeychainKind;
fn get_descriptor_secret_key() -> DescriptorSecretKey { fn get_descriptor_secret_key() -> DescriptorSecretKey {
let mnemonic = Mnemonic::from_string("chaos fabric time speed sponsor all flat solution wisdom trophy crack object robot pave observe combine where aware bench orient secret primary cable detect".to_string()).unwrap(); let mnemonic = Mnemonic::from_string("chaos fabric time speed sponsor all flat solution wisdom trophy crack object robot pave observe combine where aware bench orient secret primary cable detect".to_string()).unwrap();
@ -321,10 +324,10 @@ mod test {
let template_private_86 = let template_private_86 =
Descriptor::new_bip86(&master, KeychainKind::External, Network::Testnet); Descriptor::new_bip86(&master, KeychainKind::External, Network::Testnet);
// the extended public keys are the same when creating them manually as they are with the templates // the extended public keys are the same when creating them manually as they are with the templates
println!("Template 49: {}", template_private_49.as_string()); println!("Template 49: {}", template_private_49);
println!("Template 44: {}", template_private_44.as_string()); println!("Template 44: {}", template_private_44);
println!("Template 84: {}", template_private_84.as_string()); println!("Template 84: {}", template_private_84);
println!("Template 86: {}", template_private_86.as_string()); println!("Template 86: {}", template_private_86);
let template_public_44 = Descriptor::new_bip44_public( let template_public_44 = Descriptor::new_bip44_public(
&handmade_public_44, &handmade_public_44,
"d1d04177".to_string(), "d1d04177".to_string(),
@ -349,51 +352,51 @@ mod test {
KeychainKind::External, KeychainKind::External,
Network::Testnet, Network::Testnet,
); );
println!("Template public 49: {}", template_public_49.as_string()); println!("Template public 49: {}", template_public_49);
println!("Template public 44: {}", template_public_44.as_string()); println!("Template public 44: {}", template_public_44);
println!("Template public 84: {}", template_public_84.as_string()); println!("Template public 84: {}", template_public_84);
println!("Template public 86: {}", template_public_86.as_string()); println!("Template public 86: {}", template_public_86);
// when using a public key, both as_string and as_string_private return the same string // when using a public key, both to_string and as_string_private return the same string
assert_eq!( assert_eq!(
template_public_44.as_string_private(), template_public_44.to_string_with_secret(),
template_public_44.as_string() template_public_44.to_string()
); );
assert_eq!( assert_eq!(
template_public_49.as_string_private(), template_public_49.to_string_with_secret(),
template_public_49.as_string() template_public_49.to_string()
); );
assert_eq!( assert_eq!(
template_public_84.as_string_private(), template_public_84.to_string_with_secret(),
template_public_84.as_string() template_public_84.to_string()
); );
assert_eq!( assert_eq!(
template_public_86.as_string_private(), template_public_86.to_string_with_secret(),
template_public_86.as_string() template_public_86.to_string()
); );
// when using as_string on a private key, we get the same result as when using it on a public key // when using to_string on a private key, we get the same result as when using it on a public key
assert_eq!( assert_eq!(
template_private_44.as_string(), template_private_44.to_string(),
template_public_44.as_string() template_public_44.to_string()
); );
assert_eq!( assert_eq!(
template_private_49.as_string(), template_private_49.to_string(),
template_public_49.as_string() template_public_49.to_string()
); );
assert_eq!( assert_eq!(
template_private_84.as_string(), template_private_84.to_string(),
template_public_84.as_string() template_public_84.to_string()
); );
assert_eq!( assert_eq!(
template_private_86.as_string(), template_private_86.to_string(),
template_public_86.as_string() template_public_86.to_string()
); );
} }
#[test] #[test]
fn test_descriptor_from_string() { fn test_descriptor_from_string() {
let descriptor1 = Descriptor::new("wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEftEaNzz7dPGhWuKFU4VULesmhEfZYyBXdE/0/*)".to_string(), Network::Testnet); let descriptor1 = Descriptor::new("wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEftEaNzz7dPGhWuKFU4VULesmhEfZYyBXdE/0/*)".to_string(), Network::Testnet);
let descriptor2 = Descriptor::new("wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEftEaNzz7dPGhWuKFU4VULesmhEfZYyBXdE/0/*)".to_string(), Network::Bitcoin); let descriptor2 = Descriptor::new("wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEftEaNzz7dPGhWuKFU4VULesmhEfZYyBXdE/0/*)".to_string(), Network::Bitcoin);
// Creating a Descriptor using an extended key that doesn't match the network provided will throw and InvalidNetwork Error // Creating a Descriptor using an extended key that doesn't match the network provided will throw a DescriptorError::Key with inner InvalidNetwork error
assert!(descriptor1.is_ok()); assert!(descriptor1.is_ok());
assert_matches!(descriptor2.unwrap_err(), Alpha3Error::Generic) assert_matches!(descriptor2.unwrap_err(), DescriptorError::Key { .. });
} }
} }

101
bdk-ffi/src/electrum.rs Normal file
View File

@ -0,0 +1,101 @@
use crate::bitcoin::Transaction;
use crate::error::ElectrumError;
use crate::types::{FullScanRequest, SyncRequest};
use crate::wallet::Update;
use bdk_electrum::BdkElectrumClient as BdkBdkElectrumClient;
use bdk_electrum::{ElectrumFullScanResult, ElectrumSyncResult};
use bdk_wallet::bitcoin::Transaction as BdkTransaction;
use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk_wallet::chain::spk_client::FullScanResult as BdkFullScanResult;
use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk_wallet::chain::spk_client::SyncResult as BdkSyncResult;
use bdk_wallet::wallet::Update as BdkUpdate;
use bdk_wallet::KeychainKind;
use std::collections::BTreeMap;
use std::sync::Arc;
// NOTE: We are keeping our naming convention where the alias of the inner type is the Rust type
// prefixed with `Bdk`. In this case the inner type is `BdkElectrumClient`, so the alias is
// funnily enough named `BdkBdkElectrumClient`.
pub struct ElectrumClient(BdkBdkElectrumClient<bdk_electrum::electrum_client::Client>);
impl ElectrumClient {
pub fn new(url: String) -> Result<Self, ElectrumError> {
let inner_client: bdk_electrum::electrum_client::Client =
bdk_electrum::electrum_client::Client::new(url.as_str())?;
let client = BdkBdkElectrumClient::new(inner_client);
Ok(Self(client))
}
pub fn full_scan(
&self,
request: Arc<FullScanRequest>,
stop_gap: u64,
batch_size: u64,
fetch_prev_txouts: bool,
) -> Result<Arc<Update>, ElectrumError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkFullScanRequest<KeychainKind> = request
.0
.lock()
.unwrap()
.take()
.ok_or(ElectrumError::RequestAlreadyConsumed)?;
let electrum_result: ElectrumFullScanResult<KeychainKind> = self.0.full_scan(
request,
stop_gap as usize,
batch_size as usize,
fetch_prev_txouts,
)?;
let full_scan_result: BdkFullScanResult<KeychainKind> =
electrum_result.with_confirmation_time_height_anchor(&self.0)?;
let update = BdkUpdate {
last_active_indices: full_scan_result.last_active_indices,
graph: full_scan_result.graph_update,
chain: Some(full_scan_result.chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn sync(
&self,
request: Arc<SyncRequest>,
batch_size: u64,
fetch_prev_txouts: bool,
) -> Result<Arc<Update>, ElectrumError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkSyncRequest = request
.0
.lock()
.unwrap()
.take()
.ok_or(ElectrumError::RequestAlreadyConsumed)?;
let electrum_result: ElectrumSyncResult =
self.0
.sync(request, batch_size as usize, fetch_prev_txouts)?;
let sync_result: BdkSyncResult =
electrum_result.with_confirmation_time_height_anchor(&self.0)?;
let update = BdkUpdate {
last_active_indices: BTreeMap::default(),
graph: sync_result.graph_update,
chain: Some(sync_result.chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn broadcast(&self, transaction: &Transaction) -> Result<String, ElectrumError> {
let bdk_transaction: BdkTransaction = transaction.into();
self.0
.transaction_broadcast(&bdk_transaction)
.map_err(ElectrumError::from)
.map(|txid| txid.to_string())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +1,84 @@
use crate::error::{Alpha3Error, EsploraError}; use crate::bitcoin::Transaction;
use crate::wallet::{Update, Wallet}; use crate::error::EsploraError;
use crate::types::{FullScanRequest, SyncRequest};
use crate::wallet::Update;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::wallet::Update as BdkUpdate;
use bdk_esplora::esplora_client::{BlockingClient, Builder}; use bdk_esplora::esplora_client::{BlockingClient, Builder};
use bdk_esplora::EsploraExt; use bdk_esplora::EsploraExt;
use bdk_wallet::bitcoin::Transaction as BdkTransaction;
use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk_wallet::chain::spk_client::FullScanResult as BdkFullScanResult;
use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk_wallet::chain::spk_client::SyncResult as BdkSyncResult;
use bdk_wallet::wallet::Update as BdkUpdate;
use bdk_wallet::KeychainKind;
use crate::bitcoin::Transaction; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
pub struct EsploraClient(BlockingClient); pub struct EsploraClient(BlockingClient);
impl EsploraClient { impl EsploraClient {
pub fn new(url: String) -> Self { pub fn new(url: String) -> Self {
let client = Builder::new(url.as_str()).build_blocking().unwrap(); let client = Builder::new(url.as_str()).build_blocking();
Self(client) Self(client)
} }
// This is a temporary solution for scanning. The long-term solution involves not passing
// the wallet to the client at all.
pub fn full_scan( pub fn full_scan(
&self, &self,
wallet: Arc<Wallet>, request: Arc<FullScanRequest>,
stop_gap: u64, stop_gap: u64,
parallel_requests: u64, parallel_requests: u64,
) -> Result<Arc<Update>, EsploraError> { ) -> Result<Arc<Update>, EsploraError> {
let wallet = wallet.get_wallet(); // using option and take is not ideal but the only way to take full ownership of the request
let request: BdkFullScanRequest<KeychainKind> = request
let previous_tip = wallet.latest_checkpoint();
let keychain_spks = wallet.all_unbounded_spk_iters().into_iter().collect();
let (update_graph, last_active_indices) = self
.0 .0
.full_scan(keychain_spks, stop_gap as usize, parallel_requests as usize) .lock()
.map_err(|e| EsploraError::from(*e))?; .unwrap()
.take()
.ok_or(EsploraError::RequestAlreadyConsumed)?;
let missing_heights = update_graph.missing_heights(wallet.local_chain()); let result: BdkFullScanResult<KeychainKind> =
let chain_update = self self.0
.0 .full_scan(request, stop_gap as usize, parallel_requests as usize)?;
.update_local_chain(previous_tip, missing_heights)
.map_err(|e| EsploraError::from(*e))?;
let update = BdkUpdate { let update = BdkUpdate {
last_active_indices, last_active_indices: result.last_active_indices,
graph: update_graph, graph: result.graph_update,
chain: Some(chain_update), chain: Some(result.chain_update),
}; };
Ok(Arc::new(Update(update))) Ok(Arc::new(Update(update)))
} }
// pub fn sync(); pub fn sync(
&self,
request: Arc<SyncRequest>,
parallel_requests: u64,
) -> Result<Arc<Update>, EsploraError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkSyncRequest = request
.0
.lock()
.unwrap()
.take()
.ok_or(EsploraError::RequestAlreadyConsumed)?;
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Alpha3Error> { let result: BdkSyncResult = self.0.sync(request, parallel_requests as usize)?;
let update = BdkUpdate {
last_active_indices: BTreeMap::default(),
graph: result.graph_update,
chain: Some(result.chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
let bdk_transaction: BdkTransaction = transaction.into(); let bdk_transaction: BdkTransaction = transaction.into();
self.0 self.0
.broadcast(&bdk_transaction) .broadcast(&bdk_transaction)
.map_err(|_| Alpha3Error::Generic) .map_err(EsploraError::from)
} }
// pub fn estimate_fee();
} }

View File

@ -1,26 +1,25 @@
use crate::error::Alpha3Error; use crate::error::{Bip32Error, Bip39Error, DescriptorKeyError};
use std::fmt::Display;
use bdk::bitcoin::bip32::DerivationPath as BdkDerivationPath; use bdk_wallet::bitcoin::bip32::DerivationPath as BdkDerivationPath;
use bdk::bitcoin::key::Secp256k1; use bdk_wallet::bitcoin::key::Secp256k1;
use bdk::bitcoin::secp256k1::rand; use bdk_wallet::bitcoin::secp256k1::rand;
use bdk::bitcoin::secp256k1::rand::Rng; use bdk_wallet::bitcoin::secp256k1::rand::Rng;
use bdk::bitcoin::Network; use bdk_wallet::bitcoin::Network;
use bdk::keys::bip39::WordCount; use bdk_wallet::keys::bip39::WordCount;
use bdk::keys::bip39::{Language, Mnemonic as BdkMnemonic}; use bdk_wallet::keys::bip39::{Language, Mnemonic as BdkMnemonic};
use bdk::keys::{ use bdk_wallet::keys::{
DerivableKey, DescriptorPublicKey as BdkDescriptorPublicKey, DerivableKey, DescriptorPublicKey as BdkDescriptorPublicKey,
DescriptorSecretKey as BdkDescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey, DescriptorSecretKey as BdkDescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey,
}; };
use bdk::miniscript::descriptor::{DescriptorXKey, Wildcard}; use bdk_wallet::miniscript::descriptor::{DescriptorXKey, Wildcard};
use bdk::miniscript::BareCtx; use bdk_wallet::miniscript::BareCtx;
use std::ops::Deref; use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub(crate) struct Mnemonic { pub(crate) struct Mnemonic(BdkMnemonic);
inner: BdkMnemonic,
}
impl Mnemonic { impl Mnemonic {
pub(crate) fn new(word_count: WordCount) -> Self { pub(crate) fn new(word_count: WordCount) -> Self {
@ -32,23 +31,25 @@ impl Mnemonic {
let generated_key: GeneratedKey<_, BareCtx> = let generated_key: GeneratedKey<_, BareCtx> =
BdkMnemonic::generate_with_entropy((word_count, Language::English), entropy).unwrap(); BdkMnemonic::generate_with_entropy((word_count, Language::English), entropy).unwrap();
let mnemonic = BdkMnemonic::parse_in(Language::English, generated_key.to_string()).unwrap(); let mnemonic = BdkMnemonic::parse_in(Language::English, generated_key.to_string()).unwrap();
Mnemonic { inner: mnemonic } Mnemonic(mnemonic)
} }
pub(crate) fn from_string(mnemonic: String) -> Result<Self, Alpha3Error> { pub(crate) fn from_string(mnemonic: String) -> Result<Self, Bip39Error> {
BdkMnemonic::from_str(&mnemonic) BdkMnemonic::from_str(&mnemonic)
.map(|m| Mnemonic { inner: m }) .map(Mnemonic)
.map_err(|_| Alpha3Error::Generic) .map_err(Bip39Error::from)
} }
pub(crate) fn from_entropy(entropy: Vec<u8>) -> Result<Self, Alpha3Error> { pub(crate) fn from_entropy(entropy: Vec<u8>) -> Result<Self, Bip39Error> {
BdkMnemonic::from_entropy(entropy.as_slice()) BdkMnemonic::from_entropy(entropy.as_slice())
.map(|m| Mnemonic { inner: m }) .map(Mnemonic)
.map_err(|_| Alpha3Error::Generic) .map_err(Bip39Error::from)
} }
}
pub(crate) fn as_string(&self) -> String { impl Display for Mnemonic {
self.inner.to_string() fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
} }
} }
@ -57,23 +58,21 @@ pub(crate) struct DerivationPath {
} }
impl DerivationPath { impl DerivationPath {
pub(crate) fn new(path: String) -> Result<Self, Alpha3Error> { pub(crate) fn new(path: String) -> Result<Self, Bip32Error> {
BdkDerivationPath::from_str(&path) BdkDerivationPath::from_str(&path)
.map(|x| DerivationPath { .map(|x| DerivationPath {
inner_mutex: Mutex::new(x), inner_mutex: Mutex::new(x),
}) })
.map_err(|_| Alpha3Error::Generic) .map_err(Bip32Error::from)
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct DescriptorSecretKey { pub struct DescriptorSecretKey(pub(crate) BdkDescriptorSecretKey);
pub(crate) inner: BdkDescriptorSecretKey,
}
impl DescriptorSecretKey { impl DescriptorSecretKey {
pub(crate) fn new(network: Network, mnemonic: &Mnemonic, password: Option<String>) -> Self { pub(crate) fn new(network: Network, mnemonic: &Mnemonic, password: Option<String>) -> Self {
let mnemonic = mnemonic.inner.clone(); let mnemonic = mnemonic.0.clone();
let xkey: ExtendedKey = (mnemonic, password).into_extended_key().unwrap(); let xkey: ExtendedKey = (mnemonic, password).into_extended_key().unwrap();
let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey {
origin: None, origin: None,
@ -81,27 +80,26 @@ impl DescriptorSecretKey {
derivation_path: BdkDerivationPath::master(), derivation_path: BdkDerivationPath::master(),
wildcard: Wildcard::Unhardened, wildcard: Wildcard::Unhardened,
}); });
Self { Self(descriptor_secret_key)
inner: descriptor_secret_key,
}
} }
pub(crate) fn from_string(private_key: String) -> Result<Self, Alpha3Error> { pub(crate) fn from_string(private_key: String) -> Result<Self, DescriptorKeyError> {
let descriptor_secret_key = BdkDescriptorSecretKey::from_str(private_key.as_str()) let descriptor_secret_key = BdkDescriptorSecretKey::from_str(private_key.as_str())
.map_err(|_| Alpha3Error::Generic)?; .map_err(DescriptorKeyError::from)?;
Ok(Self { Ok(Self(descriptor_secret_key))
inner: descriptor_secret_key,
})
} }
pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, Alpha3Error> { pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor_secret_key = &self.inner; let descriptor_secret_key = &self.0;
let path = path.inner_mutex.lock().unwrap().deref().clone(); let path = path.inner_mutex.lock().unwrap().deref().clone();
match descriptor_secret_key { match descriptor_secret_key {
BdkDescriptorSecretKey::Single(_) => Err(Alpha3Error::Generic), BdkDescriptorSecretKey::Single(_) => Err(DescriptorKeyError::InvalidKeyType),
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derived_xprv = descriptor_x_key.xkey.derive_priv(&secp, &path)?; let derived_xprv = descriptor_x_key
.xkey
.derive_priv(&secp, &path)
.map_err(DescriptorKeyError::from)?;
let key_source = match descriptor_x_key.origin.clone() { let key_source = match descriptor_x_key.origin.clone() {
Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)),
None => (descriptor_x_key.xkey.fingerprint(&secp), path), None => (descriptor_x_key.xkey.fingerprint(&secp), path),
@ -112,19 +110,17 @@ impl DescriptorSecretKey {
derivation_path: BdkDerivationPath::default(), derivation_path: BdkDerivationPath::default(),
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self { Ok(Arc::new(Self(derived_descriptor_secret_key)))
inner: derived_descriptor_secret_key,
}))
} }
BdkDescriptorSecretKey::MultiXPrv(_) => Err(Alpha3Error::Generic), BdkDescriptorSecretKey::MultiXPrv(_) => Err(DescriptorKeyError::InvalidKeyType),
} }
} }
pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, Alpha3Error> { pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> {
let descriptor_secret_key = &self.inner; let descriptor_secret_key = &self.0;
let path = path.inner_mutex.lock().unwrap().deref().clone(); let path = path.inner_mutex.lock().unwrap().deref().clone();
match descriptor_secret_key { match descriptor_secret_key {
BdkDescriptorSecretKey::Single(_) => Err(Alpha3Error::Generic), BdkDescriptorSecretKey::Single(_) => Err(DescriptorKeyError::InvalidKeyType),
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let extended_path = descriptor_x_key.derivation_path.extend(path); let extended_path = descriptor_x_key.derivation_path.extend(path);
let extended_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { let extended_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey {
@ -133,24 +129,20 @@ impl DescriptorSecretKey {
derivation_path: extended_path, derivation_path: extended_path,
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self { Ok(Arc::new(Self(extended_descriptor_secret_key)))
inner: extended_descriptor_secret_key,
}))
} }
BdkDescriptorSecretKey::MultiXPrv(_) => Err(Alpha3Error::Generic), BdkDescriptorSecretKey::MultiXPrv(_) => Err(DescriptorKeyError::InvalidKeyType),
} }
} }
pub(crate) fn as_public(&self) -> Arc<DescriptorPublicKey> { pub(crate) fn as_public(&self) -> Arc<DescriptorPublicKey> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor_public_key = self.inner.to_public(&secp).unwrap(); let descriptor_public_key = self.0.to_public(&secp).unwrap();
Arc::new(DescriptorPublicKey { Arc::new(DescriptorPublicKey(descriptor_public_key))
inner: descriptor_public_key,
})
} }
pub(crate) fn secret_bytes(&self) -> Vec<u8> { pub(crate) fn secret_bytes(&self) -> Vec<u8> {
let inner = &self.inner; let inner = &self.0;
let secret_bytes: Vec<u8> = match inner { let secret_bytes: Vec<u8> = match inner {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
unreachable!() unreachable!()
@ -167,33 +159,32 @@ impl DescriptorSecretKey {
} }
pub(crate) fn as_string(&self) -> String { pub(crate) fn as_string(&self) -> String {
self.inner.to_string() self.0.to_string()
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct DescriptorPublicKey { pub struct DescriptorPublicKey(pub(crate) BdkDescriptorPublicKey);
pub(crate) inner: BdkDescriptorPublicKey,
}
impl DescriptorPublicKey { impl DescriptorPublicKey {
pub(crate) fn from_string(public_key: String) -> Result<Self, Alpha3Error> { pub(crate) fn from_string(public_key: String) -> Result<Self, DescriptorKeyError> {
let descriptor_public_key = BdkDescriptorPublicKey::from_str(public_key.as_str()) let descriptor_public_key = BdkDescriptorPublicKey::from_str(public_key.as_str())
.map_err(|_| Alpha3Error::Generic)?; .map_err(DescriptorKeyError::from)?;
Ok(Self { Ok(Self(descriptor_public_key))
inner: descriptor_public_key,
})
} }
pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, Alpha3Error> { pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor_public_key = &self.inner; let descriptor_public_key = &self.0;
let path = path.inner_mutex.lock().unwrap().deref().clone(); let path = path.inner_mutex.lock().unwrap().deref().clone();
match descriptor_public_key { match descriptor_public_key {
BdkDescriptorPublicKey::Single(_) => Err(Alpha3Error::Generic), BdkDescriptorPublicKey::Single(_) => Err(DescriptorKeyError::InvalidKeyType),
BdkDescriptorPublicKey::XPub(descriptor_x_key) => { BdkDescriptorPublicKey::XPub(descriptor_x_key) => {
let derived_xpub = descriptor_x_key.xkey.derive_pub(&secp, &path)?; let derived_xpub = descriptor_x_key
.xkey
.derive_pub(&secp, &path)
.map_err(DescriptorKeyError::from)?;
let key_source = match descriptor_x_key.origin.clone() { let key_source = match descriptor_x_key.origin.clone() {
Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)),
None => (descriptor_x_key.xkey.fingerprint(), path), None => (descriptor_x_key.xkey.fingerprint(), path),
@ -204,19 +195,17 @@ impl DescriptorPublicKey {
derivation_path: BdkDerivationPath::default(), derivation_path: BdkDerivationPath::default(),
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self { Ok(Arc::new(Self(derived_descriptor_public_key)))
inner: derived_descriptor_public_key,
}))
} }
BdkDescriptorPublicKey::MultiXPub(_) => Err(Alpha3Error::Generic), BdkDescriptorPublicKey::MultiXPub(_) => Err(DescriptorKeyError::InvalidKeyType),
} }
} }
pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, Alpha3Error> { pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> {
let descriptor_public_key = &self.inner; let descriptor_public_key = &self.0;
let path = path.inner_mutex.lock().unwrap().deref().clone(); let path = path.inner_mutex.lock().unwrap().deref().clone();
match descriptor_public_key { match descriptor_public_key {
BdkDescriptorPublicKey::Single(_) => Err(Alpha3Error::Generic), BdkDescriptorPublicKey::Single(_) => Err(DescriptorKeyError::InvalidKeyType),
BdkDescriptorPublicKey::XPub(descriptor_x_key) => { BdkDescriptorPublicKey::XPub(descriptor_x_key) => {
let extended_path = descriptor_x_key.derivation_path.extend(path); let extended_path = descriptor_x_key.derivation_path.extend(path);
let extended_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey { let extended_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey {
@ -225,25 +214,22 @@ impl DescriptorPublicKey {
derivation_path: extended_path, derivation_path: extended_path,
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self { Ok(Arc::new(Self(extended_descriptor_public_key)))
inner: extended_descriptor_public_key,
}))
} }
BdkDescriptorPublicKey::MultiXPub(_) => Err(Alpha3Error::Generic), BdkDescriptorPublicKey::MultiXPub(_) => Err(DescriptorKeyError::InvalidKeyType),
} }
} }
pub(crate) fn as_string(&self) -> String { pub(crate) fn as_string(&self) -> String {
self.inner.to_string() self.0.to_string()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::error::DescriptorKeyError;
use crate::keys::{DerivationPath, DescriptorPublicKey, DescriptorSecretKey, Mnemonic}; use crate::keys::{DerivationPath, DescriptorPublicKey, DescriptorSecretKey, Mnemonic};
// use bdk::bitcoin::hashes::hex::ToHex; use bdk_wallet::bitcoin::Network;
use crate::error::Alpha3Error;
use bdk::bitcoin::Network;
use std::sync::Arc; use std::sync::Arc;
fn get_inner() -> DescriptorSecretKey { fn get_inner() -> DescriptorSecretKey {
@ -254,7 +240,7 @@ mod test {
fn derive_dsk( fn derive_dsk(
key: &DescriptorSecretKey, key: &DescriptorSecretKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorSecretKey>, Alpha3Error> { ) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = DerivationPath::new(path.to_string()).unwrap();
key.derive(&path) key.derive(&path)
} }
@ -262,7 +248,7 @@ mod test {
fn extend_dsk( fn extend_dsk(
key: &DescriptorSecretKey, key: &DescriptorSecretKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorSecretKey>, Alpha3Error> { ) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = DerivationPath::new(path.to_string()).unwrap();
key.extend(&path) key.extend(&path)
} }
@ -270,7 +256,7 @@ mod test {
fn derive_dpk( fn derive_dpk(
key: &DescriptorPublicKey, key: &DescriptorPublicKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorPublicKey>, Alpha3Error> { ) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = DerivationPath::new(path.to_string()).unwrap();
key.derive(&path) key.derive(&path)
} }
@ -278,7 +264,7 @@ mod test {
fn extend_dpk( fn extend_dpk(
key: &DescriptorPublicKey, key: &DescriptorPublicKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorPublicKey>, Alpha3Error> { ) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = DerivationPath::new(path.to_string()).unwrap();
key.extend(&path) key.extend(&path)
} }

View File

@ -1,44 +1,73 @@
mod bitcoin; mod bitcoin;
mod descriptor; mod descriptor;
mod electrum;
mod error; mod error;
mod esplora; mod esplora;
mod keys; mod keys;
mod store;
mod types; mod types;
mod wallet; mod wallet;
use crate::bitcoin::Address; use crate::bitcoin::Address;
use crate::bitcoin::Amount;
use crate::bitcoin::FeeRate;
use crate::bitcoin::OutPoint; use crate::bitcoin::OutPoint;
use crate::bitcoin::PartiallySignedTransaction; use crate::bitcoin::Psbt;
use crate::bitcoin::Script; use crate::bitcoin::Script;
use crate::bitcoin::Transaction; use crate::bitcoin::Transaction;
use crate::bitcoin::TxIn;
use crate::bitcoin::TxOut; use crate::bitcoin::TxOut;
use crate::descriptor::Descriptor; use crate::descriptor::Descriptor;
use crate::error::Alpha3Error; use crate::electrum::ElectrumClient;
use crate::error::AddressParseError;
use crate::error::Bip32Error;
use crate::error::Bip39Error;
use crate::error::CalculateFeeError; use crate::error::CalculateFeeError;
use crate::error::CannotConnectError;
use crate::error::CreateTxError;
use crate::error::DescriptorError;
use crate::error::DescriptorKeyError;
use crate::error::ElectrumError;
use crate::error::EsploraError; use crate::error::EsploraError;
use crate::error::ExtractTxError;
use crate::error::FeeRateError;
use crate::error::FromScriptError;
use crate::error::InspectError;
use crate::error::ParseAmountError;
use crate::error::PersistenceError;
use crate::error::PsbtError;
use crate::error::PsbtParseError;
use crate::error::SignerError;
use crate::error::SqliteError;
use crate::error::TransactionError;
use crate::error::TxidParseError;
use crate::error::WalletCreationError;
use crate::esplora::EsploraClient; use crate::esplora::EsploraClient;
use crate::keys::DerivationPath; use crate::keys::DerivationPath;
use crate::keys::DescriptorPublicKey; use crate::keys::DescriptorPublicKey;
use crate::keys::DescriptorSecretKey; use crate::keys::DescriptorSecretKey;
use crate::keys::Mnemonic; use crate::keys::Mnemonic;
use crate::types::AddressIndex; use crate::store::SqliteStore;
use crate::types::AddressInfo; use crate::types::AddressInfo;
use crate::types::Balance; use crate::types::Balance;
use crate::types::FeeRate; use crate::types::CanonicalTx;
use crate::types::ChainPosition;
use crate::types::ChangeSet;
use crate::types::FullScanRequest;
use crate::types::FullScanScriptInspector;
use crate::types::LocalOutput; use crate::types::LocalOutput;
use crate::types::ScriptAmount; use crate::types::ScriptAmount;
use crate::types::SyncRequest;
use crate::types::SyncScriptInspector;
use crate::wallet::BumpFeeTxBuilder; use crate::wallet::BumpFeeTxBuilder;
use crate::wallet::SentAndReceivedValues; use crate::wallet::SentAndReceivedValues;
use crate::wallet::TxBuilder; use crate::wallet::TxBuilder;
use crate::wallet::Update; use crate::wallet::Update;
use crate::wallet::Wallet; use crate::wallet::Wallet;
use crate::error::WalletCreationError; use bdk_wallet::bitcoin::Network;
use bdk::bitcoin::Network; use bdk_wallet::keys::bip39::WordCount;
use bdk::keys::bip39::WordCount; use bdk_wallet::wallet::tx_builder::ChangeSpendPolicy;
use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk_wallet::KeychainKind;
use bdk::KeychainKind;
uniffi::include_scaffolding!("bdk"); uniffi::include_scaffolding!("bdk");
// TODO: TxIn, Payload

39
bdk-ffi/src/store.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::error::SqliteError;
use crate::types::ChangeSet;
use bdk_sqlite::rusqlite::Connection;
use bdk_sqlite::{Store as BdkSqliteStore, Store};
use bdk_wallet::chain::ConfirmationTimeHeightAnchor;
use bdk_wallet::KeychainKind;
use std::sync::{Arc, Mutex, MutexGuard};
pub struct SqliteStore(Mutex<BdkSqliteStore<KeychainKind, ConfirmationTimeHeightAnchor>>);
impl SqliteStore {
pub fn new(path: String) -> Result<Self, SqliteError> {
let connection = Connection::open(path)?;
let db = Store::new(connection)?;
Ok(Self(Mutex::new(db)))
}
pub(crate) fn get_store(
&self,
) -> MutexGuard<BdkSqliteStore<KeychainKind, ConfirmationTimeHeightAnchor>> {
self.0.lock().expect("sqlite store")
}
pub fn write(&self, changeset: &ChangeSet) -> Result<(), SqliteError> {
self.get_store()
.write(&changeset.0)
.map_err(SqliteError::from)
}
pub fn read(&self) -> Result<Option<Arc<ChangeSet>>, SqliteError> {
self.get_store()
.read()
.map_err(SqliteError::from)
.map(|optional_bdk_change_set| optional_bdk_change_set.map(ChangeSet::from))
.map(|optional_change_set| optional_change_set.map(Arc::new))
}
}

View File

@ -1,40 +1,52 @@
use crate::bitcoin::{Address, OutPoint, Script, TxOut}; use crate::bitcoin::Amount;
use crate::bitcoin::{Address, OutPoint, Script, Transaction, TxOut};
use crate::InspectError;
use bdk::wallet::AddressIndex as BdkAddressIndex; use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf;
use bdk::wallet::AddressInfo as BdkAddressInfo; use bdk_wallet::bitcoin::Transaction as BdkTransaction;
use bdk::wallet::Balance as BdkBalance; use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk::KeychainKind; use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk_wallet::chain::tx_graph::CanonicalTx as BdkCanonicalTx;
use bdk_wallet::chain::{ChainPosition as BdkChainPosition, ConfirmationTimeHeightAnchor};
use bdk_wallet::wallet::AddressInfo as BdkAddressInfo;
use bdk_wallet::wallet::Balance as BdkBalance;
use bdk_wallet::KeychainKind;
use bdk_wallet::LocalOutput as BdkLocalOutput;
use bdk::LocalOutput as BdkLocalOutput; use bdk_electrum::bdk_chain::CombinedChangeSet;
use std::sync::{Arc, Mutex};
use bdk::FeeRate as BdkFeeRate; #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainPosition {
Confirmed { height: u32, timestamp: u64 },
Unconfirmed { timestamp: u64 },
}
use std::sync::Arc; pub struct CanonicalTx {
pub transaction: Arc<Transaction>,
pub chain_position: ChainPosition,
}
#[derive(Clone, Debug)] impl From<BdkCanonicalTx<'_, Arc<BdkTransaction>, ConfirmationTimeHeightAnchor>> for CanonicalTx {
pub struct FeeRate(pub BdkFeeRate); fn from(tx: BdkCanonicalTx<'_, Arc<BdkTransaction>, ConfirmationTimeHeightAnchor>) -> Self {
let chain_position = match tx.chain_position {
BdkChainPosition::Confirmed(anchor) => ChainPosition::Confirmed {
height: anchor.confirmation_height,
timestamp: anchor.confirmation_time,
},
BdkChainPosition::Unconfirmed(timestamp) => ChainPosition::Unconfirmed { timestamp },
};
impl FeeRate { CanonicalTx {
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self { transaction: Arc::new(Transaction::from(tx.tx_node.tx.as_ref().clone())),
FeeRate(BdkFeeRate::from_sat_per_vb(sat_per_vb)) chain_position,
} }
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
FeeRate(BdkFeeRate::from_sat_per_kwu(sat_per_kwu))
}
pub fn as_sat_per_vb(&self) -> f32 {
self.0.as_sat_per_vb()
}
pub fn sat_per_kwu(&self) -> f32 {
self.0.sat_per_kwu()
} }
} }
pub struct ScriptAmount { pub struct ScriptAmount {
pub script: Arc<Script>, pub script: Arc<Script>,
pub amount: u64, pub amount: Arc<Amount>,
} }
pub struct AddressInfo { pub struct AddressInfo {
@ -53,75 +65,36 @@ impl From<BdkAddressInfo> for AddressInfo {
} }
} }
pub enum AddressIndex {
New,
LastUnused,
Peek { index: u32 },
}
impl From<AddressIndex> for BdkAddressIndex {
fn from(address_index: AddressIndex) -> Self {
match address_index {
AddressIndex::New => BdkAddressIndex::New,
AddressIndex::LastUnused => BdkAddressIndex::LastUnused,
AddressIndex::Peek { index } => BdkAddressIndex::Peek(index),
}
}
}
impl From<BdkAddressIndex> for AddressIndex {
fn from(address_index: BdkAddressIndex) -> Self {
match address_index {
BdkAddressIndex::New => AddressIndex::New,
BdkAddressIndex::LastUnused => AddressIndex::LastUnused,
_ => panic!("Mmmm not working"),
}
}
}
// TODO 9: Peek is not correctly implemented
impl From<&AddressIndex> for BdkAddressIndex {
fn from(address_index: &AddressIndex) -> Self {
match address_index {
AddressIndex::New => BdkAddressIndex::New,
AddressIndex::LastUnused => BdkAddressIndex::LastUnused,
AddressIndex::Peek { index } => BdkAddressIndex::Peek(*index),
}
}
}
impl From<&BdkAddressIndex> for AddressIndex {
fn from(address_index: &BdkAddressIndex) -> Self {
match address_index {
BdkAddressIndex::New => AddressIndex::New,
BdkAddressIndex::LastUnused => AddressIndex::LastUnused,
_ => panic!("Mmmm not working"),
}
}
}
pub struct Balance { pub struct Balance {
pub immature: u64, pub immature: Arc<Amount>,
pub trusted_pending: u64, pub trusted_pending: Arc<Amount>,
pub untrusted_pending: u64, pub untrusted_pending: Arc<Amount>,
pub confirmed: u64, pub confirmed: Arc<Amount>,
pub trusted_spendable: u64, pub trusted_spendable: Arc<Amount>,
pub total: u64, pub total: Arc<Amount>,
} }
impl From<BdkBalance> for Balance { impl From<BdkBalance> for Balance {
fn from(bdk_balance: BdkBalance) -> Self { fn from(bdk_balance: BdkBalance) -> Self {
Balance { Balance {
immature: bdk_balance.immature, immature: Arc::new(bdk_balance.immature.into()),
trusted_pending: bdk_balance.trusted_pending, trusted_pending: Arc::new(bdk_balance.trusted_pending.into()),
untrusted_pending: bdk_balance.untrusted_pending, untrusted_pending: Arc::new(bdk_balance.untrusted_pending.into()),
confirmed: bdk_balance.confirmed, confirmed: Arc::new(bdk_balance.confirmed.into()),
trusted_spendable: bdk_balance.trusted_spendable(), trusted_spendable: Arc::new(bdk_balance.trusted_spendable().into()),
total: bdk_balance.total(), total: Arc::new(bdk_balance.total().into()),
} }
} }
} }
pub struct ChangeSet(pub(crate) CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>);
impl From<CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>> for ChangeSet {
fn from(change_set: CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>) -> Self {
ChangeSet(change_set)
}
}
pub struct LocalOutput { pub struct LocalOutput {
pub outpoint: OutPoint, pub outpoint: OutPoint,
pub txout: TxOut, pub txout: TxOut,
@ -137,7 +110,7 @@ impl From<BdkLocalOutput> for LocalOutput {
vout: local_utxo.outpoint.vout, vout: local_utxo.outpoint.vout,
}, },
txout: TxOut { txout: TxOut {
value: local_utxo.txout.value, value: local_utxo.txout.value.to_sat(),
script_pubkey: Arc::new(Script(local_utxo.txout.script_pubkey)), script_pubkey: Arc::new(Script(local_utxo.txout.script_pubkey)),
}, },
keychain: local_utxo.keychain, keychain: local_utxo.keychain,
@ -145,3 +118,56 @@ impl From<BdkLocalOutput> for LocalOutput {
} }
} }
} }
// Callback for the FullScanRequest
pub trait FullScanScriptInspector: Sync + Send {
fn inspect(&self, keychain: KeychainKind, index: u32, script: Arc<Script>);
}
// Callback for the SyncRequest
pub trait SyncScriptInspector: Sync + Send {
fn inspect(&self, script: Arc<Script>, total: u64);
}
pub struct FullScanRequest(pub(crate) Mutex<Option<BdkFullScanRequest<KeychainKind>>>);
pub struct SyncRequest(pub(crate) Mutex<Option<BdkSyncRequest>>);
impl SyncRequest {
pub fn inspect_spks(
&self,
inspector: Arc<dyn SyncScriptInspector>,
) -> Result<Arc<Self>, InspectError> {
let mut guard = self.0.lock().unwrap();
if let Some(sync_request) = guard.take() {
let total = sync_request.spks.len() as u64;
let sync_request = sync_request.inspect_spks(move |spk| {
inspector.inspect(Arc::new(BdkScriptBuf::from(spk).into()), total)
});
Ok(Arc::new(SyncRequest(Mutex::new(Some(sync_request)))))
} else {
Err(InspectError::RequestAlreadyConsumed)
}
}
}
impl FullScanRequest {
pub fn inspect_spks_for_all_keychains(
&self,
inspector: Arc<dyn FullScanScriptInspector>,
) -> Result<Arc<Self>, InspectError> {
let mut guard = self.0.lock().unwrap();
if let Some(full_scan_request) = guard.take() {
let inspector = Arc::new(inspector);
let full_scan_request =
full_scan_request.inspect_spks_for_all_keychains(move |k, spk_i, script| {
inspector.inspect(k, spk_i, Arc::new(BdkScriptBuf::from(script).into()))
});
Ok(Arc::new(FullScanRequest(Mutex::new(Some(
full_scan_request,
)))))
} else {
Err(InspectError::RequestAlreadyConsumed)
}
}
}

View File

@ -1,87 +1,87 @@
use crate::bitcoin::{OutPoint, PartiallySignedTransaction, Transaction}; use crate::bitcoin::Amount;
use crate::bitcoin::{FeeRate, OutPoint, Psbt, Script, Transaction};
use crate::descriptor::Descriptor; use crate::descriptor::Descriptor;
use crate::error::{Alpha3Error, CalculateFeeError, WalletCreationError}; use crate::error::{
use crate::types::ScriptAmount; CalculateFeeError, CannotConnectError, CreateTxError, SignerError, TxidParseError,
use crate::types::{Balance, FeeRate}; WalletCreationError,
use crate::Script; };
use crate::{AddressIndex, AddressInfo}; use crate::types::{
AddressInfo, Balance, CanonicalTx, ChangeSet, FullScanRequest, LocalOutput, ScriptAmount,
SyncRequest,
};
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; use bdk_wallet::bitcoin::amount::Amount as BdkAmount;
use bdk::bitcoin::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction; use bdk_wallet::bitcoin::Network;
use bdk::bitcoin::Network; use bdk_wallet::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid}; use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf;
use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk_wallet::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid};
use bdk::wallet::{ChangeSet, Update as BdkUpdate}; use bdk_wallet::chain::{CombinedChangeSet, ConfirmationTimeHeightAnchor};
use bdk::Wallet as BdkWallet; use bdk_wallet::wallet::tx_builder::ChangeSpendPolicy;
use bdk::{FeeRate as BdkFeeRate, SignOptions}; use bdk_wallet::wallet::Update as BdkUpdate;
use bdk_file_store::Store; use bdk_wallet::Wallet as BdkWallet;
use bdk_wallet::{KeychainKind, SignOptions};
use std::collections::HashSet; use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
const MAGIC_BYTES: &[u8] = "bdkffi".as_bytes();
pub struct Wallet { pub struct Wallet {
inner_mutex: Mutex<BdkWallet<Store<ChangeSet>>>, inner_mutex: Mutex<BdkWallet>,
} }
impl Wallet { impl Wallet {
pub fn new( pub fn new(
descriptor: Arc<Descriptor>, descriptor: Arc<Descriptor>,
change_descriptor: Option<Arc<Descriptor>>, change_descriptor: Arc<Descriptor>,
persistence_backend_path: String,
network: Network, network: Network,
) -> Result<Self, WalletCreationError> { ) -> Result<Self, WalletCreationError> {
let descriptor = descriptor.as_string_private(); let descriptor = descriptor.to_string_with_secret();
let change_descriptor = change_descriptor.map(|d| d.as_string_private()); let change_descriptor = change_descriptor.to_string_with_secret();
let db = Store::<ChangeSet>::open_or_create_new(MAGIC_BYTES, persistence_backend_path)?; let wallet: BdkWallet = BdkWallet::new(&descriptor, &change_descriptor, network)?;
let wallet: bdk::wallet::Wallet<Store<ChangeSet>> =
BdkWallet::new_or_load(&descriptor, change_descriptor.as_ref(), db, network)?;
Ok(Wallet { Ok(Wallet {
inner_mutex: Mutex::new(wallet), inner_mutex: Mutex::new(wallet),
}) })
} }
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet<Store<ChangeSet>>> { pub fn new_or_load(
descriptor: Arc<Descriptor>,
change_descriptor: Arc<Descriptor>,
change_set: Option<Arc<ChangeSet>>,
network: Network,
) -> Result<Self, WalletCreationError> {
let descriptor = descriptor.to_string_with_secret();
let change_descriptor = change_descriptor.to_string_with_secret();
let change_set: Option<CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>> =
change_set.map(|cs| cs.0.clone());
let wallet: BdkWallet =
BdkWallet::new_or_load(&descriptor, &change_descriptor, change_set, network)?;
Ok(Wallet {
inner_mutex: Mutex::new(wallet),
})
}
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet") self.inner_mutex.lock().expect("wallet")
} }
pub fn get_address(&self, address_index: AddressIndex) -> AddressInfo { pub fn reveal_next_address(&self, keychain_kind: KeychainKind) -> AddressInfo {
self.get_wallet() self.get_wallet().reveal_next_address(keychain_kind).into()
.try_get_address(address_index.into())
.unwrap()
.into()
} }
pub fn apply_update(&self, update: Arc<Update>) -> Result<(), Alpha3Error> { pub fn apply_update(&self, update: Arc<Update>) -> Result<(), CannotConnectError> {
self.get_wallet() self.get_wallet()
.apply_update(update.0.clone()) .apply_update(update.0.clone())
.map_err(|_| Alpha3Error::Generic) .map_err(CannotConnectError::from)
}
// TODO: This is the fallible version of get_internal_address; should I rename it to get_internal_address?
// It's a slight change of the API, the other option is to rename the get_address to try_get_address
pub fn try_get_internal_address(
&self,
address_index: AddressIndex,
) -> Result<AddressInfo, Alpha3Error> {
self.get_wallet()
.try_get_internal_address(address_index.into())
.map_or_else(
|_| Err(Alpha3Error::Generic),
|address_info| Ok(address_info.into()),
)
} }
pub fn network(&self) -> Network { pub fn network(&self) -> Network {
self.get_wallet().network() self.get_wallet().network()
} }
pub fn get_balance(&self) -> Balance { pub fn balance(&self) -> Balance {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance(); let bdk_balance = self.get_wallet().balance();
Balance::from(bdk_balance) Balance::from(bdk_balance)
} }
@ -91,30 +91,41 @@ impl Wallet {
pub(crate) fn sign( pub(crate) fn sign(
&self, &self,
psbt: Arc<PartiallySignedTransaction>, psbt: Arc<Psbt>,
// sign_options: Option<SignOptions>, // sign_options: Option<SignOptions>,
) -> Result<bool, Alpha3Error> { ) -> Result<bool, SignerError> {
let mut psbt = psbt.inner.lock().unwrap(); let mut psbt = psbt.0.lock().unwrap();
self.get_wallet() self.get_wallet()
.sign(&mut psbt, SignOptions::default()) .sign(&mut psbt, SignOptions::default())
.map_err(|_| Alpha3Error::Generic) .map_err(SignerError::from)
} }
pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues { pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues {
let (sent, received): (u64, u64) = self.get_wallet().sent_and_received(&tx.into()); let (sent, received) = self.get_wallet().sent_and_received(&tx.into());
SentAndReceivedValues { sent, received } SentAndReceivedValues {
sent: Arc::new(sent.into()),
received: Arc::new(received.into()),
}
} }
pub fn transactions(&self) -> Vec<Arc<Transaction>> { pub fn transactions(&self) -> Vec<CanonicalTx> {
self.get_wallet() self.get_wallet()
.transactions() .transactions()
.map(|tx| Arc::new(tx.tx_node.tx.into())) .map(|tx| tx.into())
.collect() .collect()
} }
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> { pub fn get_tx(&self, txid: String) -> Result<Option<CanonicalTx>, TxidParseError> {
let txid =
Txid::from_str(txid.as_str()).map_err(|_| TxidParseError::InvalidTxid { txid })?;
Ok(self.get_wallet().get_tx(txid).map(|tx| tx.into()))
}
pub fn calculate_fee(&self, tx: &Transaction) -> Result<Arc<Amount>, CalculateFeeError> {
self.get_wallet() self.get_wallet()
.calculate_fee(&tx.into()) .calculate_fee(&tx.into())
.map(Amount::from)
.map(Arc::new)
.map_err(|e| e.into()) .map_err(|e| e.into())
} }
@ -124,205 +135,48 @@ impl Wallet {
.map(|bdk_fee_rate| Arc::new(FeeRate(bdk_fee_rate))) .map(|bdk_fee_rate| Arc::new(FeeRate(bdk_fee_rate)))
.map_err(|e| e.into()) .map_err(|e| e.into())
} }
pub fn list_unspent(&self) -> Vec<LocalOutput> {
self.get_wallet().list_unspent().map(|o| o.into()).collect()
}
pub fn list_output(&self) -> Vec<LocalOutput> {
self.get_wallet().list_output().map(|o| o.into()).collect()
}
pub fn start_full_scan(&self) -> Arc<FullScanRequest> {
let request = self.get_wallet().start_full_scan();
Arc::new(FullScanRequest(Mutex::new(Some(request))))
}
pub fn start_sync_with_revealed_spks(&self) -> Arc<SyncRequest> {
let request = self.get_wallet().start_sync_with_revealed_spks();
Arc::new(SyncRequest(Mutex::new(Some(request))))
}
pub fn take_staged(&self) -> Option<Arc<ChangeSet>> {
self.get_wallet()
.take_staged()
.map(|change_set| Arc::new(change_set.into()))
}
} }
pub struct SentAndReceivedValues { pub struct SentAndReceivedValues {
pub sent: u64, pub sent: Arc<Amount>,
pub received: u64, pub received: Arc<Amount>,
} }
pub struct Update(pub(crate) BdkUpdate); pub struct Update(pub(crate) BdkUpdate);
// /// A Bitcoin wallet.
// /// The Wallet acts as a way of coherently interfacing with output descriptors and related transactions. Its main components are:
// /// 1. Output descriptors from which it can derive addresses.
// /// 2. A Database where it tracks transactions and utxos related to the descriptors.
// /// 3. Signers that can contribute signatures to addresses instantiated from the descriptors.
// impl Wallet {
// pub fn new(
// descriptor: Arc<Descriptor>,
// change_descriptor: Option<Arc<Descriptor>>,
// network: Network,
// ) -> Result<Self, BdkError> {
// let wallet = BdkWallet::new_no_persist()?;
// Ok(Wallet {
// inner: wallet,
// })
// }
// }
// /// Return whether or not a script is part of this wallet (either internal or external).
// pub(crate) fn is_mine(&self, script: Arc<Script>) -> bool {
// self.inner.is_mine(&script.inner)
// }
//
// /// Sync the internal database with the blockchain.
// // pub(crate) fn sync(
// // &self,
// // blockchain: &Blockchain,
// // progress: Option<Box<dyn Progress>>,
// // ) -> Result<(), BdkError> {
// // let bdk_sync_opts = BdkSyncOptions {
// // progress: progress.map(|p| {
// // Box::new(ProgressHolder { progress: p })
// // as Box<(dyn bdk::blockchain::Progress + 'static)>
// // }),
// // };
// //
// // let blockchain = blockchain.get_blockchain();
// // self.get_wallet().sync(blockchain.deref(), bdk_sync_opts)
// // }
//
// /// Return a derived address using the external descriptor, see AddressIndex for available address index selection
// /// strategies. If none of the keys in the descriptor are derivable (i.e. the descriptor does not end with a * character)
// /// then the same address will always be returned for any AddressIndex.
// /// MIGRATION 1.0: The wallet needs to be mutated for this method to work... does that mean I should bring back the Mutex?
// /// Is this thread-safe?
// pub(crate) fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo {
// AddressInfo::from(self.inner.get_address(address_index.into()))
// }
//
// /// Return a derived address using the internal (change) descriptor.
// ///
// /// If the wallet doesn't have an internal descriptor it will use the external descriptor.
// ///
// /// see [`AddressIndex`] for available address index selection strategies. If none of the keys
// /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
// /// be returned for any [`AddressIndex`].
// pub(crate) fn get_internal_address(&mut self, address_index: AddressIndex, ) -> AddressInfo {
// AddressInfo::from(self.inner.get_internal_address(address_index.into()))
// }
//
// /// Return the balance, meaning the sum of this wallets unspent outputs values. Note that this method only operates
// /// on the internal database, which first needs to be Wallet.sync manually.
// pub(crate) fn get_balance(&self) -> Balance {
// Balance::from(self.inner.get_balance())
// }
//
// /// Sign a transaction with all the wallet's signers, in the order specified by every signer's
// /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that
// /// has the value true if the PSBT was finalized, or false otherwise.
// ///
// /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way
// /// the transaction is finalized at the end. Note that it can't be guaranteed that *every*
// /// signers will follow the options, but the "software signers" (WIF keys and `xprv`) defined
// /// in this library will.
// pub(crate) fn sign(
// &self,
// psbt: &PartiallySignedTransaction,
// sign_options: Option<SignOptions>,
// ) -> Result<bool, BdkError> {
// let mut psbt = psbt.inner.lock().unwrap();
// self.inner.sign(
// &mut psbt,
// sign_options.map(SignOptions::into).unwrap_or_default(),
// )
// }
//
// /// Return the list of transactions made and received by the wallet. Note that this method only operate on the internal database, which first needs to be [Wallet.sync] manually.
// pub(crate) fn list_transactions(
// &self,
// include_raw: bool,
// ) -> Result<Vec<TransactionDetails>, BdkError> {
// let transaction_details = self.inner.list_transactions(include_raw)?;
// Ok(transaction_details
// .into_iter()
// .map(TransactionDetails::from)
// .collect())
// }
//
// /// Return the list of unspent outputs of this wallet. Note that this method only operates on the internal database,
// /// which first needs to be Wallet.sync manually.
// pub(crate) fn list_unspent(&self) -> Result<Vec<LocalUtxo>, BdkError> {
// let unspents: Vec<BdkLocalUtxo> = self.inner.list_unspent()?;
// Ok(unspents.into_iter().map(LocalUtxo::from).collect())
// }
// }
//
// /// Options for a software signer
// ///
// /// Adjust the behavior of our software signers and the way a transaction is finalized
// #[derive(Debug, Clone, Default)]
// pub struct SignOptions {
// /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
// /// provided
// ///
// /// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into
// /// paying a fee larger than expected.
// ///
// /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
// /// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
// /// should correctly produce a signature, at the expense of an increased trust in the creator
// /// of the PSBT.
// ///
// /// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
// pub trust_witness_utxo: bool,
//
// /// Whether the wallet should assume a specific height has been reached when trying to finalize
// /// a transaction
// ///
// /// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
// /// timelock height has already been reached. This option allows overriding the "current height" to let the
// /// wallet use timelocks in the future to spend a coin.
// pub assume_height: Option<u32>,
//
// /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
// /// what its value is
// ///
// /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
// pub allow_all_sighashes: bool,
//
// /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
// ///
// /// Defaults to `true` which will remove partial signatures during finalization.
// pub remove_partial_sigs: bool,
//
// /// Whether to try finalizing the PSBT after the inputs are signed.
// ///
// /// Defaults to `true` which will try finalizing PSBT after inputs are signed.
// pub try_finalize: bool,
//
// // Specifies which Taproot script-spend leaves we should sign for. This option is
// // ignored if we're signing a non-taproot PSBT.
// //
// // Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
// // TODO pub tap_leaves_options: TapLeavesOptions,
// /// Whether we should try to sign a taproot transaction with the taproot internal key
// /// or not. This option is ignored if we're signing a non-taproot PSBT.
// ///
// /// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
// pub sign_with_tap_internal_key: bool,
//
// /// Whether we should grind ECDSA signature to ensure signing with low r
// /// or not.
// /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
// pub allow_grinding: bool,
// }
//
// impl From<SignOptions> for BdkSignOptions {
// fn from(sign_options: SignOptions) -> Self {
// BdkSignOptions {
// trust_witness_utxo: sign_options.trust_witness_utxo,
// assume_height: sign_options.assume_height,
// allow_all_sighashes: sign_options.allow_all_sighashes,
// remove_partial_sigs: sign_options.remove_partial_sigs,
// try_finalize: sign_options.try_finalize,
// tap_leaves_options: Default::default(),
// sign_with_tap_internal_key: sign_options.sign_with_tap_internal_key,
// allow_grinding: sign_options.allow_grinding,
// }
// }
// }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TxBuilder { pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>, pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>,
pub(crate) utxos: Vec<OutPoint>, pub(crate) utxos: Vec<OutPoint>,
pub(crate) unspendable: HashSet<OutPoint>, pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) change_policy: ChangeSpendPolicy, pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) manually_selected_only: bool, pub(crate) manually_selected_only: bool,
pub(crate) fee_rate: Option<FeeRate>, pub(crate) fee_rate: Option<FeeRate>,
pub(crate) fee_absolute: Option<u64>, pub(crate) fee_absolute: Option<Arc<Amount>>,
pub(crate) drain_wallet: bool, pub(crate) drain_wallet: bool,
pub(crate) drain_to: Option<BdkScriptBuf>, pub(crate) drain_to: Option<BdkScriptBuf>,
pub(crate) rbf: Option<RbfValue>, pub(crate) rbf: Option<RbfValue>,
@ -346,9 +200,9 @@ impl TxBuilder {
} }
} }
pub(crate) fn add_recipient(&self, script: &Script, amount: u64) -> Arc<Self> { pub(crate) fn add_recipient(&self, script: &Script, amount: Arc<Amount>) -> Arc<Self> {
let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone(); let mut recipients: Vec<(BdkScriptBuf, BdkAmount)> = self.recipients.clone();
recipients.append(&mut vec![(script.0.clone(), amount)]); recipients.append(&mut vec![(script.0.clone(), amount.0)]);
Arc::new(TxBuilder { Arc::new(TxBuilder {
recipients, recipients,
@ -359,7 +213,7 @@ impl TxBuilder {
pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> { pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> {
let recipients = recipients let recipients = recipients
.iter() .iter()
.map(|script_amount| (script_amount.script.0.clone(), script_amount.amount)) .map(|script_amount| (script_amount.script.0.clone(), script_amount.amount.0)) //;
.collect(); .collect();
Arc::new(TxBuilder { Arc::new(TxBuilder {
recipients, recipients,
@ -431,7 +285,7 @@ impl TxBuilder {
}) })
} }
pub(crate) fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> { pub(crate) fn fee_absolute(&self, fee_amount: Arc<Amount>) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
fee_absolute: Some(fee_amount), fee_absolute: Some(fee_amount),
..self.clone() ..self.clone()
@ -466,18 +320,7 @@ impl TxBuilder {
}) })
} }
/// Add data as an output using OP_RETURN. pub(crate) fn finish(&self, wallet: &Arc<Wallet>) -> Result<Arc<Psbt>, CreateTxError> {
// pub(crate) fn add_data(&self, data: Vec<u8>) -> Arc<Self> {
// Arc::new(TxBuilder {
// data,
// ..self.clone()
// })
// }
pub(crate) fn finish(
&self,
wallet: &Arc<Wallet>,
) -> Result<Arc<PartiallySignedTransaction>, Alpha3Error> {
// TODO: I had to change the wallet here to be mutable. Why is that now required with the 1.0 API? // TODO: I had to change the wallet here to be mutable. Why is that now required with the 1.0 API?
let mut wallet = wallet.get_wallet(); let mut wallet = wallet.get_wallet();
let mut tx_builder = wallet.build_tx(); let mut tx_builder = wallet.build_tx();
@ -487,8 +330,9 @@ impl TxBuilder {
tx_builder.change_policy(self.change_policy); tx_builder.change_policy(self.change_policy);
if !self.utxos.is_empty() { if !self.utxos.is_empty() {
let bdk_utxos: Vec<BdkOutPoint> = self.utxos.iter().map(BdkOutPoint::from).collect(); let bdk_utxos: Vec<BdkOutPoint> = self.utxos.iter().map(BdkOutPoint::from).collect();
let utxos: &[BdkOutPoint] = &bdk_utxos; tx_builder
tx_builder.add_utxos(utxos)?; .add_utxos(&bdk_utxos)
.map_err(CreateTxError::from)?;
} }
if !self.unspendable.is_empty() { if !self.unspendable.is_empty() {
let bdk_unspendable: Vec<BdkOutPoint> = let bdk_unspendable: Vec<BdkOutPoint> =
@ -501,8 +345,8 @@ impl TxBuilder {
if let Some(fee_rate) = &self.fee_rate { if let Some(fee_rate) = &self.fee_rate {
tx_builder.fee_rate(fee_rate.0); tx_builder.fee_rate(fee_rate.0);
} }
if let Some(fee_amount) = self.fee_absolute { if let Some(fee_amount) = &self.fee_absolute {
tx_builder.fee_absolute(fee_amount); tx_builder.fee_absolute(fee_amount.0);
} }
if self.drain_wallet { if self.drain_wallet {
tx_builder.drain_wallet(); tx_builder.drain_wallet();
@ -520,11 +364,8 @@ impl TxBuilder {
} }
} }
} }
// if !&self.data.is_empty() {
// tx_builder.add_data(self.data.as_slice());
// }
let psbt = tx_builder.finish()?; let psbt = tx_builder.finish().map_err(CreateTxError::from)?;
Ok(Arc::new(psbt.into())) Ok(Arc::new(psbt.into()))
} }
@ -533,28 +374,19 @@ impl TxBuilder {
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct BumpFeeTxBuilder { pub(crate) struct BumpFeeTxBuilder {
pub(crate) txid: String, pub(crate) txid: String,
pub(crate) fee_rate: f32, pub(crate) fee_rate: Arc<FeeRate>,
pub(crate) allow_shrinking: Option<Arc<Script>>,
pub(crate) rbf: Option<RbfValue>, pub(crate) rbf: Option<RbfValue>,
} }
impl BumpFeeTxBuilder { impl BumpFeeTxBuilder {
pub(crate) fn new(txid: String, fee_rate: f32) -> Self { pub(crate) fn new(txid: String, fee_rate: Arc<FeeRate>) -> Self {
Self { Self {
txid, txid,
fee_rate, fee_rate,
allow_shrinking: None,
rbf: None, rbf: None,
} }
} }
pub(crate) fn allow_shrinking(&self, script_pubkey: Arc<Script>) -> Arc<Self> {
Arc::new(Self {
allow_shrinking: Some(script_pubkey),
..self.clone()
})
}
pub(crate) fn enable_rbf(&self) -> Arc<Self> { pub(crate) fn enable_rbf(&self) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
rbf: Some(RbfValue::Default), rbf: Some(RbfValue::Default),
@ -569,17 +401,13 @@ impl BumpFeeTxBuilder {
}) })
} }
pub(crate) fn finish( pub(crate) fn finish(&self, wallet: &Wallet) -> Result<Arc<Psbt>, CreateTxError> {
&self, let txid = Txid::from_str(self.txid.as_str()).map_err(|_| CreateTxError::UnknownUtxo {
wallet: &Wallet, outpoint: self.txid.clone(),
) -> Result<Arc<PartiallySignedTransaction>, Alpha3Error> { })?;
let txid = Txid::from_str(self.txid.as_str()).map_err(|_| Alpha3Error::Generic)?;
let mut wallet = wallet.get_wallet(); let mut wallet = wallet.get_wallet();
let mut tx_builder = wallet.build_fee_bump(txid)?; let mut tx_builder = wallet.build_fee_bump(txid).map_err(CreateTxError::from)?;
tx_builder.fee_rate(BdkFeeRate::from_sat_per_vb(self.fee_rate)); tx_builder.fee_rate(self.fee_rate.0);
if let Some(allow_shrinking) = &self.allow_shrinking {
tx_builder.allow_shrinking(allow_shrinking.0.clone())?;
}
if let Some(rbf) = &self.rbf { if let Some(rbf) = &self.rbf {
match *rbf { match *rbf {
RbfValue::Default => { RbfValue::Default => {
@ -590,8 +418,7 @@ impl BumpFeeTxBuilder {
} }
} }
} }
let psbt: BdkPartiallySignedTransaction = let psbt: BdkPsbt = tx_builder.finish()?;
tx_builder.finish().map_err(|_| Alpha3Error::Generic)?;
Ok(Arc::new(psbt.into())) Ok(Arc::new(psbt.into()))
} }

View File

@ -4,6 +4,6 @@
*/ */
import Foundation import Foundation
import bdk import BitcoinDevKit
let network = Network.testnet let network = Network.testnet

View File

@ -5,8 +5,6 @@ cdylib_name = "bdkffi"
[bindings.python] [bindings.python]
cdylib_name = "bdkffi" cdylib_name = "bdkffi"
[bindings.ruby]
cdylib_name = "bdkffi"
[bindings.swift] [bindings.swift]
module_name = "BitcoinDevKit"
cdylib_name = "bdkffi" cdylib_name = "bdkffi"

View File

@ -1,5 +1,5 @@
# bdk-jvm # bdk-jvm
This project builds a .jar package for the JVM platform that provide Kotlin language bindings for the [`bdk`] library. The Kotlin language bindings are created by the `bdk-ffi` project which is included in the root of this repository. This project builds a .jar package for the JVM platform that provides Kotlin language bindings for the [`bdk`] library. The Kotlin language bindings are created by the `bdk-ffi` project which is included in the root of this repository.
## How to Use ## How to Use
To use the Kotlin language bindings for [`bdk`] in your JVM project add the following to your gradle dependencies: To use the Kotlin language bindings for [`bdk`] in your JVM project add the following to your gradle dependencies:
@ -13,33 +13,6 @@ dependencies {
} }
``` ```
You may then import and use the `org.bitcoindevkit` library in your Kotlin code like so. Note that this example is for the `0.30.0` release. For examples of the 1.0 API in the alpha releases, take a look at the tests [here](https://github.com/bitcoindevkit/bdk-ffi/tree/master/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit).
```kotlin
import org.bitcoindevkit.*
// ...
val externalDescriptor = Descriptor("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", Network.TESTNET)
val internalDescriptor = Descriptor("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)", Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/")
val wallet: Wallet = Wallet(
descriptor = externalDescriptor,
changeDescriptor = internalDescriptor,
persistenceBackendPath = "./bdkwallet.db",
network = Network.TESTNET
)
val update = esploraClient.fullScan(
wallet = wallet,
stopGap = 10uL,
parallelRequests = 1uL
)
wallet.applyUpdate(update)
val newAddress = wallet.getAddress(AddressIndex.LastUnused)
```
### Snapshot releases ### Snapshot releases
To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block: To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block:
```kotlin ```kotlin
@ -56,17 +29,17 @@ dependencies {
* [Tatooine Faucet](https://github.com/thunderbiscuit/tatooine) * [Tatooine Faucet](https://github.com/thunderbiscuit/tatooine)
## How to build ## How to build
_Note that Kotlin version `1.6.10` or later is required to build the library._ _Note that Kotlin version `1.9.23` or later is required to build the library._
1. Install JDK 11. It must be version 11 (not 17), otherwise it won't build. For example, with SDKMAN!: 1. Install JDK 17. For example, with SDKMAN!:
```shell ```shell
curl -s "https://get.sdkman.io" | bash curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh" source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 11.0.19-tem sdk install java 17.0.2-tem
``` ```
2. Install Rust (note that we are currently building using Rust 1.73.0): 2. Install Rust (note that we are currently building using Rust 1.77.1):
```shell ```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default 1.73.0 rustup default 1.77.1
``` ```
3. Clone this repository. 3. Clone this repository.
```shell ```shell
@ -84,7 +57,7 @@ rustup target add x86_64-apple-darwin aarch64-apple-darwin
## How to publish to your local Maven repo ## How to publish to your local Maven repo
```shell ```shell
cd bdk-jvm cd bdk-jvm
./gradlew publishToMavenLocal --exclude-task signMavenPublication ./gradlew publishToMavenLocal -P localBuild
``` ```
Note that the commands assume you don't need the local libraries to be signed. If you do wish to sign them, simply set your `~/.gradle/gradle.properties` signing key values like so: Note that the commands assume you don't need the local libraries to be signed. If you do wish to sign them, simply set your `~/.gradle/gradle.properties` signing key values like so:
@ -93,7 +66,7 @@ signing.gnupg.keyName=<YOUR_GNUPG_ID>
signing.gnupg.passphrase=<YOUR_GNUPG_PASSPHRASE> signing.gnupg.passphrase=<YOUR_GNUPG_PASSPHRASE>
``` ```
and use the `publishToMavenLocal` task without excluding the signing task: and use the `publishToMavenLocal` task without the `localBuild` flag:
```shell ```shell
./gradlew publishToMavenLocal ./gradlew publishToMavenLocal
``` ```

View File

@ -1,4 +1,9 @@
plugins { plugins {
id("org.jetbrains.kotlin.jvm").version("1.9.23").apply(false)
id("org.gradle.java-library")
id("org.gradle.maven-publish")
id("org.gradle.signing")
id("org.bitcoindevkit.plugins.generate-jvm-bindings").apply(false)
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
} }

View File

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=true android.enableJetifier=true
kotlin.code.style=official kotlin.code.style=official
libraryVersion=1.0.0-alpha.2b-SNAPSHOT libraryVersion=1.0.0-alpha.12-SNAPSHOT

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
bdk-jvm/gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in # shell script including quotes and variable substitutions, so put them in
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
bdk-jvm/gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

23
bdk-jvm/justfile Normal file
View File

@ -0,0 +1,23 @@
default:
just --list
build:
./gradlew buildJvmLib
clean:
rm -rf ../bdk-ffi/target/
rm -rf ./build/
rm -rf ./lib/build/
rm -rf ./plugins/build/
publish-local:
./gradlew publishToMavenLocal -P localBuild
test:
./gradlew test
test-offline:
./gradlew test -P excludeConnectedTests
test-specific TEST:
./gradlew test --tests {{TEST}}

View File

@ -6,19 +6,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
val libraryVersion: String by project val libraryVersion: String by project
plugins { plugins {
id("org.jetbrains.kotlin.jvm") version "1.6.10" id("org.jetbrains.kotlin.jvm")
id("java-library") id("org.gradle.java-library")
id("maven-publish") id("org.gradle.maven-publish")
id("signing") id("org.gradle.signing")
// Custom plugin to generate the native libs and bindings file // Custom plugin to generate the native libs and bindings file
id("org.bitcoindevkit.plugins.generate-jvm-bindings") id("org.bitcoindevkit.plugins.generate-jvm-bindings")
} }
repositories {
mavenCentral()
}
java { java {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
@ -28,20 +24,23 @@ java {
// This block ensures that the tests that require access to a blockchain are not // This block ensures that the tests that require access to a blockchain are not
// run if the -P excludeConnectedTests flag is passed to gradle. // run if the -P excludeConnectedTests flag is passed to gradle.
// This ensures our CI runs are not fickle by not requiring access to testnet. // This ensures our CI runs are not fickle by not requiring access to testnet or signet.
// This is a workaround until we have a proper regtest setup for the CI. // This is a workaround until we have a proper regtest setup for the CI.
// Note that the command in the CI is ./gradlew test -P excludeConnectedTests // Note that the command in the CI is ./gradlew test -P excludeConnectedTests
tasks.test { tasks.test {
if (project.hasProperty("excludeConnectedTests")) { if (project.hasProperty("excludeConnectedTests")) {
exclude("**/LiveWalletTest.class") exclude("**/LiveElectrumClientTest.class")
exclude("**/LiveMemoryWalletTest.class")
exclude("**/LiveTransactionTest.class")
exclude("**/LiveTxBuilderTest.class") exclude("**/LiveTxBuilderTest.class")
exclude("**/LiveWalletTest.class")
} }
} }
testing { testing {
suites { suites {
val test by getting(JvmTestSuite::class) { val test by getting(JvmTestSuite::class) {
useKotlinTest("1.6.10") useKotlinTest("1.9.23")
} }
} }
} }
@ -92,6 +91,13 @@ afterEvaluate {
url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT")
} }
} }
developers {
developer {
id.set("bdkdevelopers")
name.set("Bitcoin Dev Kit Developers")
email.set("dev@bitcoindevkit.org")
}
}
scm { scm {
connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git")
developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git")
@ -104,6 +110,10 @@ afterEvaluate {
} }
signing { signing {
if (project.hasProperty("localBuild")) {
isRequired = false
}
val signingKeyId: String? by project val signingKeyId: String? by project
val signingKey: String? by project val signingKey: String? by project
val signingPassword: String? by project val signingPassword: String? by project

View File

@ -0,0 +1,39 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602"
class LiveElectrumClientTest {
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@Test
fun testSyncedBalance() {
val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val electrumClient: ElectrumClient = ElectrumClient(SIGNET_ELECTRUM_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = electrumClient.fullScan(fullScanRequest, 10uL, 10uL, false)
wallet.applyUpdate(update)
println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.transaction.computeTxid()}")
println("Sent ${sentAndReceived.sent.toSat()}")
println("Received ${sentAndReceived.received.toSat()}")
}
}
}

View File

@ -0,0 +1,67 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveMemoryWalletTest {
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@Test
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.transaction.computeTxid()}")
println("Sent ${sentAndReceived.sent.toSat()}")
println("Received ${sentAndReceived.received.toSat()}")
}
}
@Test
fun testScriptInspector() {
val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val scriptInspector: FullScriptInspector = FullScriptInspector()
val fullScanRequest: FullScanRequest = wallet.startFullScan().inspectSpksForAllKeychains(scriptInspector)
val update = esploraClient.fullScan(fullScanRequest, 21uL, 1uL)
wallet.applyUpdate(update)
println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
}
}
class FullScriptInspector: FullScanScriptInspector {
override fun inspect(keychain: KeychainKind, index: UInt, script: Script){
println("Inspecting index $index for keychain $keychain")
}
}

View File

@ -0,0 +1,47 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveTransactionTests {
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@Test
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
println("Wallet balance: ${wallet.balance().total.toSat()}")
assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val transaction: Transaction = wallet.transactions().first().transaction
println("First transaction:")
println("Txid: ${transaction.computeTxid()}")
println("Version: ${transaction.version()}")
println("Total size: ${transaction.totalSize()}")
println("Vsize: ${transaction.vsize()}")
println("Weight: ${transaction.weight()}")
println("Coinbase transaction: ${transaction.isCoinbase()}")
println("Is explicitly RBF: ${transaction.isExplicitlyRbf()}")
println("Inputs: ${transaction.input()}")
println("Outputs: ${transaction.output()}")
}
}

View File

@ -5,11 +5,22 @@ import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveTxBuilderTest { class LiveTxBuilderTest {
private val persistenceFilePath = run { private val persistenceFilePath = run {
val currentDirectory = System.getProperty("user.dir") val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db" "$currentDirectory/bdk_persistence.sqlite"
} }
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -22,18 +33,21 @@ class LiveTxBuilderTest {
@Test @Test
fun testTxBuilder() { fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val update = esploraClient.fullScan(wallet, 10uL, 1uL) val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2.0f)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@ -43,26 +57,27 @@ class LiveTxBuilderTest {
@Test @Test
fun complexTxBuilder() { fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET) val fullScanRequest: FullScanRequest = wallet.startFullScan()
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.TESTNET) val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf( val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL), ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), 4200uL), ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
) )
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.setRecipients(allRecipients) .setRecipients(allRecipients)
.feeRate(FeeRate.fromSatPerVb(4.0f)) .feeRate(FeeRate.fromSatPerVb(4uL))
.changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN) .changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN)
.enableRbf() .enableRbf()
.finish(wallet) .finish(wallet)

View File

@ -5,11 +5,22 @@ import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveWalletTest { class LiveWalletTest {
private val persistenceFilePath = run { private val persistenceFilePath = run {
val currentDirectory = System.getProperty("user.dir") val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db" "$currentDirectory/bdk_persistence.sqlite"
} }
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -21,22 +32,22 @@ class LiveWalletTest {
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val esploraClient: EsploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val fullScanRequest: FullScanRequest = wallet.startFullScan()
// val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api") val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
// val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
println("Transactions count: ${wallet.transactions().count()}") println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3) val transactions = wallet.transactions().take(3)
for (tx in transactions) { for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx) val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.txid()}") println("Transaction: ${tx.transaction.computeTxid()}")
println("Sent ${sentAndReceived.sent}") println("Sent ${sentAndReceived.sent}")
println("Received ${sentAndReceived.received}") println("Received ${sentAndReceived.received}")
} }
@ -44,24 +55,23 @@ class LiveWalletTest {
@Test @Test
fun testBroadcastTransaction() { fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.TESTNET) val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val update = esploraClient.fullScan(wallet, 10uL, 1uL) val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.balance().total.toSat()}")
println("New address: ${wallet.getAddress(AddressIndex.New).address.asString()}")
assert(wallet.getBalance().total > 0uL) { assert(wallet.balance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again." "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
} }
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: PartiallySignedTransaction = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2.0f)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@ -71,13 +81,13 @@ class LiveWalletTest {
assertTrue(walletDidSign) assertTrue(walletDidSign)
val tx: Transaction = psbt.extractTx() val tx: Transaction = psbt.extractTx()
println("Txid is: ${tx.txid()}") println("Txid is: ${tx.computeTxid()}")
val txFee: ULong = wallet.calculateFee(tx) val txFee: Amount = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}") println("Tx fee is: ${txFee}")
val feeRate: FeeRate = wallet.calculateFeeRate(tx) val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.asSatPerVb()} sat/vB") println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")
esploraClient.broadcast(tx) esploraClient.broadcast(tx)
} }

View File

@ -12,7 +12,7 @@ class OfflineDescriptorTest {
assertEquals( assertEquals(
expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx",
actual = descriptor.asString() actual = descriptor.toString()
) )
} }
} }

View File

@ -0,0 +1,45 @@
package org.bitcoindevkit
import kotlin.test.Test
import kotlin.test.assertEquals
class OfflinePersistenceTest {
private val persistenceFilePath = run {
val currentDirectory = System.getProperty("user.dir")
val dbFileName = "pre_existing_wallet_persistence_test.sqlite"
"$currentDirectory/src/test/resources/$dbFileName"
}
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
@Test
fun testPersistence() {
val sqliteStore: SqliteStore = SqliteStore(persistenceFilePath)
val initialChangeSet: ChangeSet? = sqliteStore.read()
requireNotNull(initialChangeSet) { "ChangeSet should not be null after loading a valid database" }
val wallet: Wallet = Wallet.newOrLoad(
descriptor,
changeDescriptor,
initialChangeSet,
Network.SIGNET,
)
val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
println("Address: $addressInfo")
assertEquals(
expected = 7u,
actual = addressInfo.index,
)
assertEquals(
expected = "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2",
actual = addressInfo.address.toString(),
)
}
}

View File

@ -10,8 +10,16 @@ import kotlin.test.assertFalse
class OfflineWalletTest { class OfflineWalletTest {
private val persistenceFilePath = run { private val persistenceFilePath = run {
val currentDirectory = System.getProperty("user.dir") val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db" "$currentDirectory/bdk_persistence.sqlite"
} }
private val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.TESTNET
)
private val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.TESTNET
)
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@ -27,22 +35,17 @@ class OfflineWalletTest {
val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null)
val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET)
assertTrue(descriptor.asString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'") assertTrue(descriptor.toString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'")
} }
@Test @Test
fun testNewAddress() { fun testNewAddress() {
val descriptor: Descriptor = Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet(
descriptor, descriptor,
null, changeDescriptor,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network")
assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network")
@ -50,27 +53,22 @@ class OfflineWalletTest {
assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be")
assertEquals( assertEquals(
expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", expected = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
actual = addressInfo.address.asString() actual = addressInfo.address.toString()
) )
} }
@Test @Test
fun testBalance() { fun testBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet(
descriptor, descriptor,
null, changeDescriptor,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
assertEquals( assertEquals(
expected = 0uL, expected = 0uL,
actual = wallet.getBalance().total actual = wallet.balance().total.toSat()
) )
} }
} }

View File

@ -112,20 +112,20 @@ internal class UniFfiJvmPlugin : Plugin<Project> {
// TODO 2: Is the Windows name the correct one? // TODO 2: Is the Windows name the correct one?
// TODO 3: This will not work on mac Intel (x86_64 architecture) // TODO 3: This will not work on mac Intel (x86_64 architecture)
// val libraryPath = when (operatingSystem) { val libraryPath = when (operatingSystem) {
// OS.LINUX -> "./target/x86_64-unknown-linux-gnu/release-smaller/libbdkffi.so" OS.LINUX -> "./target/x86_64-unknown-linux-gnu/release-smaller/libbdkffi.so"
// OS.MAC -> "./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib" OS.MAC -> "./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib"
// OS.WINDOWS -> "./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll" OS.WINDOWS -> "./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll"
// else -> throw Exception("Unsupported OS") else -> throw Exception("Unsupported OS")
// } }
// workingDir("${project.projectDir}/../../bdk-ffi/") workingDir("${project.projectDir}/../../bdk-ffi/")
// val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "--library", libraryPath, "--language", "kotlin", "--out-dir", "../bdk-jvm/lib/src/main/kotlin/", "--no-format") val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "--library", libraryPath, "--language", "kotlin", "--out-dir", "../bdk-jvm/lib/src/main/kotlin/", "--no-format")
// The code above was for the migration to uniffi 0.24.3 using the --library flag // The code above was for the migration to uniffi 0.24.3 using the --library flag
// The code below works with uniffi 0.23.0 // The code below works with uniffi 0.23.0
workingDir("${project.projectDir}/../../bdk-ffi/") // workingDir("${project.projectDir}/../../bdk-ffi/")
val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "src/bdk.udl", "--language", "kotlin", "--out-dir", "../bdk-jvm/lib/src/main/kotlin", "--no-format") // val cargoArgs: List<String> = listOf("run", "--bin", "uniffi-bindgen", "generate", "src/bdk.udl", "--language", "kotlin", "--out-dir", "../bdk-jvm/lib/src/main/kotlin", "--no-format")
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)

View File

@ -2,3 +2,15 @@ rootProject.name = "bdk-jvm"
include(":lib") include(":lib")
includeBuild("plugins") includeBuild("plugins")
pluginManagement {
repositories {
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}

14
bdk-python/justfile Normal file
View File

@ -0,0 +1,14 @@
default:
just --list
build-local-mac:
bash ./scripts/generate-macos-arm64.sh && python3 setup.py bdist_wheel --verbose
clean:
rm -rf ../bdk-ffi/target/
rm -rf ./bdkpython.egg-info/
rm -rf ./build/
rm -rf ./dist/
test:
python3 -m unittest --verbose

View File

@ -4,14 +4,15 @@ set -euo pipefail
${PYBIN}/python --version ${PYBIN}/python --version
${PYBIN}/pip install -r requirements.txt ${PYBIN}/pip install -r requirements.txt
echo "Generating bdk.py..."
cd ../bdk-ffi/ cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format rustup default 1.77.1
echo "Generating native binaries..." echo "Generating native binaries..."
rustup default 1.73.0
cargo build --profile release-smaller cargo build --profile release-smaller
echo "Generating bdk.py..."
cargo run --bin uniffi-bindgen generate --library ./target/release-smaller/libbdkffi.so --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
echo "Copying linux libbdkffi.so..." echo "Copying linux libbdkffi.so..."
cp ./target/release-smaller/libbdkffi.so ../bdk-python/src/bdkpython/libbdkffi.so cp ./target/release-smaller/libbdkffi.so ../bdk-python/src/bdkpython/libbdkffi.so

View File

@ -2,17 +2,18 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install --user -r requirements.txt pip install -r requirements.txt
echo "Generating bdk.py..."
cd ../bdk-ffi/ cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format rustup default 1.77.1
rustup target add aarch64-apple-darwin
echo "Generating native binaries..." echo "Generating native binaries..."
rustup default 1.73.0
rustup target add aarch64-apple-darwin
cargo build --profile release-smaller --target aarch64-apple-darwin cargo build --profile release-smaller --target aarch64-apple-darwin
echo "Generating bdk.py..."
cargo run --bin uniffi-bindgen generate --library ./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
echo "Copying libraries libbdkffi.dylib..." echo "Copying libraries libbdkffi.dylib..."
cp ./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib cp ./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib

View File

@ -2,17 +2,18 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install --user -r requirements.txt pip install -r requirements.txt
echo "Generating bdk.py..."
cd ../bdk-ffi/ cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format rustup default 1.77.1
rustup target add x86_64-apple-darwin
echo "Generating native binaries..." echo "Generating native binaries..."
rustup default 1.73.0
rustup target add x86_64-apple-darwin
cargo build --profile release-smaller --target x86_64-apple-darwin cargo build --profile release-smaller --target x86_64-apple-darwin
echo "Generating bdk.py..."
cargo run --bin uniffi-bindgen generate --library ./target/x86_64-apple-darwin/release-smaller/libbdkffi.dylib --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
echo "Copying libraries libbdkffi.dylib..." echo "Copying libraries libbdkffi.dylib..."
cp ./target/x86_64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib cp ./target/x86_64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib

View File

@ -2,17 +2,18 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install --user -r requirements.txt pip install -r requirements.txt
echo "Generating bdk.py..."
cd ../bdk-ffi/ cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format rustup default 1.77.1
rustup target add x86_64-pc-windows-msvc
echo "Generating native binaries..." echo "Generating native binaries..."
rustup default 1.73.0
rustup target add x86_64-pc-windows-msvc
cargo build --profile release-smaller --target x86_64-pc-windows-msvc cargo build --profile release-smaller --target x86_64-pc-windows-msvc
echo "Generating bdk.py..."
cargo run --bin uniffi-bindgen generate --library ./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
echo "Copying libraries bdkffi.dll..." echo "Copying libraries bdkffi.dll..."
cp ./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll ../bdk-python/src/bdkpython/bdkffi.dll cp ./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll ../bdk-python/src/bdkpython/bdkffi.dll

View File

@ -18,7 +18,7 @@ import bdkpython as bdk
setup( setup(
name="bdkpython", name="bdkpython",
version="1.0.0a2.dev1", version="1.0.0a12.dev",
description="The Python language bindings for the Bitcoin Development Kit", description="The Python language bindings for the Bitcoin Development Kit",
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -27,6 +27,7 @@ setup(
packages=["bdkpython"], packages=["bdkpython"],
package_dir={"bdkpython": "./src/bdkpython"}, package_dir={"bdkpython": "./src/bdkpython"},
url="https://github.com/bitcoindevkit/bdk-ffi", url="https://github.com/bitcoindevkit/bdk-ffi",
author="Bitcoin Dev Kit Developers <dev@bitcoindevkit.org>",
license="MIT or Apache 2.0", license="MIT or Apache 2.0",
# This is required to ensure the library name includes the python version, abi, and platform tags # This is required to ensure the library name includes the python version, abi, and platform tags
# See issue #350 for more information # See issue #350 for more information

View File

@ -2,81 +2,93 @@ import bdkpython as bdk
import unittest import unittest
import os import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
bdk.Network.TESTNET
)
class LiveTxBuilderTest(unittest.TestCase): class LiveTxBuilderTest(unittest.TestCase):
def tearDown(self) -> None: def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"): if os.path.exists("./bdk_persistence.sqlite"):
os.remove("./bdk_persistence.db") os.remove("./bdk_persistence.sqlite")
def test_tx_builder(self): def test_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET bdk.Network.SIGNET
)
wallet: bdk.Wallet = bdk.Wallet(
descriptor,
None,
"./bdk_persistence.db",
bdk.Network.TESTNET
)
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://esplora.testnet.kuutamo.cloud/")
update = esploraClient.full_scan(
wallet = wallet,
stop_gap = 10,
parallel_requests = 1
)
wallet.apply_update(update)
self.assertGreater(wallet.get_balance().total, 0)
recipient = bdk.Address(
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET
)
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
def complex_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
descriptor, descriptor,
change_descriptor, change_descriptor,
"./bdk_persistence.db", bdk.Network.SIGNET
bdk.Network.TESTNET
) )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://esplora.testnet.kuutamo.cloud/") esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL)
update = esploraClient.full_scan( full_scan_request: bdk.FullScanRequest = wallet.start_full_scan()
wallet = wallet, update = esplora_client.full_scan(
stop_gap = 10, full_scan_request=full_scan_request,
parallel_requests = 1 stop_gap=10,
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
self.assertGreater(wallet.get_balance().total, 0) self.assertGreater(
wallet.balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address} and try again."
)
recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
)
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
def complex_tx_builder(self):
wallet: bdk.Wallet = bdk.Wallet(
descriptor,
change_descriptor,
bdk.Network.SIGNET
)
esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL)
full_scan_request: bdk.FullScanRequest = wallet.start_full_scan()
update = esplora_client.full_scan(
full_scan_request=full_scan_request,
stop_gap=10,
parallel_requests=1
)
wallet.apply_update(update)
self.assertGreater(
wallet.balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address} and try again."
)
recipient1 = bdk.Address( recipient1 = bdk.Address(
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET network=bdk.Network.SIGNET
) )
recipient2 = bdk.Address( recipient2 = bdk.Address(
address = "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", address="tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6",
network = bdk.Network.TESTNET network=bdk.Network.SIGNET
) )
all_recipients = list( all_recipients = list(
bdk.ScriptAmount(recipient1.script_pubkey, 4200), bdk.ScriptAmount(recipient1.script_pubkey, 4200),
bdk.ScriptAmount(recipient2.script_pubkey, 4200) bdk.ScriptAmount(recipient2.script_pubkey, 4200)
) )
psbt: bdk.PartiallySignedTransaction = bdk.TxBuilder().set_recipients(all_recipients).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).enable_rbf().finish(wallet) psbt: bdk.Psbt = bdk.TxBuilder().set_recipients(all_recipients).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).enable_rbf().finish(wallet)
wallet.sign(psbt) wallet.sign(psbt)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")

View File

@ -2,82 +2,93 @@ import bdkpython as bdk
import unittest import unittest
import os import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
bdk.Network.TESTNET
)
class LiveWalletTest(unittest.TestCase): class LiveWalletTest(unittest.TestCase):
def tearDown(self) -> None: def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"): if os.path.exists("./bdk_persistence.sqlite"):
os.remove("./bdk_persistence.db") os.remove("./bdk_persistence.sqlite")
def test_synced_balance(self): def test_synced_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET
)
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
descriptor, descriptor,
None, change_descriptor,
"./bdk_persistence.db", bdk.Network.SIGNET
bdk.Network.TESTNET
) )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://esplora.testnet.kuutamo.cloud/") esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL)
update = esploraClient.full_scan( full_scan_request: bdk.FullScanRequest = wallet.start_full_scan()
wallet = wallet, update = esplora_client.full_scan(
stop_gap = 10, full_scan_request=full_scan_request,
parallel_requests = 1 stop_gap=10,
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
self.assertGreater(wallet.get_balance().total, 0) self.assertGreater(
wallet.balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address} and try again."
)
print(f"Transactions count: {len(wallet.transactions())}") print(f"Transactions count: {len(wallet.transactions())}")
transactions = wallet.transactions()[:3] transactions = wallet.transactions()[:3]
for tx in transactions: for tx in transactions:
sent_and_received = wallet.sent_and_received(tx) sent_and_received = wallet.sent_and_received(tx.transaction)
print(f"Transaction: {tx.txid()}") print(f"Transaction: {tx.transaction.compute_txid()}")
print(f"Sent {sent_and_received.sent}") print(f"Sent {sent_and_received.sent.to_sat()}")
print(f"Received {sent_and_received.received}") print(f"Received {sent_and_received.received.to_sat()}")
def test_broadcast_transaction(self): def test_broadcast_transaction(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET
)
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
descriptor, descriptor,
None, change_descriptor,
"./bdk_persistence.db", bdk.Network.SIGNET
bdk.Network.TESTNET
) )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://esplora.testnet.kuutamo.cloud/") esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL)
update = esploraClient.full_scan( full_scan_request: bdk.FullScanRequest = wallet.start_full_scan()
wallet = wallet, update = esplora_client.full_scan(
stop_gap = 10, full_scan_request=full_scan_request,
parallel_requests = 1 stop_gap=10,
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
self.assertGreater(wallet.get_balance().total, 0) self.assertGreater(
wallet.balance().total.to_sat(),
recipient = bdk.Address( 0,
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address} and try again."
network = bdk.Network.TESTNET
) )
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet) recipient = bdk.Address(
# print(psbt.serialize()) address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
)
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
walletDidSign = wallet.sign(psbt) walletDidSign = wallet.sign(psbt)
self.assertTrue(walletDidSign) self.assertTrue(walletDidSign)
tx = psbt.extract_tx() tx = psbt.extract_tx()
print(f"Transaction Id: {tx.txid()}") print(f"Transaction Id: {tx.compute_txid()}")
fee = wallet.calculate_fee(tx) fee = wallet.calculate_fee(tx)
print(f"Transaction Fee: {fee}") print(f"Transaction Fee: {fee.to_sat()}")
fee_rate = wallet.calculate_fee_rate(tx) fee_rate = wallet.calculate_fee_rate(tx)
print(f"Transaction Fee Rate: {fee_rate.as_sat_per_vb()} sat/vB") print(f"Transaction Fee Rate: {fee_rate.to_sat_per_vb_ceil()} sat/vB")
esploraClient.broadcast(tx) esplora_client.broadcast(tx)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -10,7 +10,7 @@ class OfflineDescriptorTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
"tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx",
descriptor.as_string() descriptor.__str__()
) )

View File

@ -2,45 +2,44 @@ import bdkpython as bdk
import unittest import unittest
import os import os
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
bdk.Network.TESTNET
)
class OfflineWalletTest(unittest.TestCase): class OfflineWalletTest(unittest.TestCase):
def tearDown(self) -> None: def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"): if os.path.exists("./bdk_persistence.sqlite"):
os.remove("./bdk_persistence.db") os.remove("./bdk_persistence.sqlite")
def test_new_address(self): def test_new_address(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
wallet: Wallet = bdk.Wallet( wallet: Wallet = bdk.Wallet(
descriptor, descriptor,
None, change_descriptor,
"./bdk_persistence.db",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW()) address_info: bdk.AddressInfo = wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL)
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network") self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network")
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.SIGNET), "Address is not valid for signet network") self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.SIGNET), "Address is not valid for signet network")
self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.REGTEST), "Address is valid for regtest network, but it shouldn't be")
self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be")
self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string()) self.assertEqual("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address_info.address.__str__())
def test_balance(self): def test_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
descriptor, descriptor,
None, change_descriptor,
"./bdk_persistence.db",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
self.assertEqual(wallet.get_balance().total, 0) self.assertEqual(wallet.balance().total.to_sat(), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -33,6 +33,10 @@ let package = Package(
), ),
.testTarget( .testTarget(
name: "BitcoinDevKitTests", name: "BitcoinDevKitTests",
dependencies: ["BitcoinDevKit"]), dependencies: ["BitcoinDevKit"],
resources: [
.copy("Resources/pre_existing_wallet_persistence_test.sqlite")
]
),
] ]
) )

View File

@ -25,7 +25,7 @@ import BitcoinDevKit
Swift Package Manager releases for `bdk-swift` are published to a separate repository (https://github.com/bitcoindevkit/bdk-swift), and that is where the releases are created for it. Swift Package Manager releases for `bdk-swift` are published to a separate repository (https://github.com/bitcoindevkit/bdk-swift), and that is where the releases are created for it.
The `bdk-swift/build-local-swift.sh` script can be used instead to create a version of the project for local testing. The `bdk-swift/build-xcframework.sh` script can be used instead to create a version of the project for local testing.
### How to test ### How to test

View File

@ -0,0 +1,48 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602"
final class LiveElectrumClientTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
func testSyncedBalance() throws {
let wallet = try Wallet(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
network: Network.signet
)
let electrumClient: ElectrumClient = try ElectrumClient(url: SIGNET_ELECTRUM_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try electrumClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
batchSize: 10,
fetchPrevTxouts: false
)
try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address
XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3)
for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.computeTxid())")
print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received.toSat())")
}
}
}

View File

@ -0,0 +1,80 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveMemoryWalletTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
func testSyncedBalance() throws {
let wallet = try Wallet(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3)
for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.computeTxid())")
print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received.toSat())")
}
}
func testScriptInspector() throws {
let wallet = try Wallet(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let scriptInspector = FullScriptInspector()
let fullScanRequest = try wallet.startFullScan().inspectSpksForAllKeychains(inspector: scriptInspector)
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 21,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
}
}
class FullScriptInspector: FullScanScriptInspector {
func inspect(keychain: KeychainKind, index: UInt32, script: Script) {
print("Inspecting index \(index) for keychain \(keychain)")
}
}

View File

@ -0,0 +1,56 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTransactionTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
func testSyncedBalance() throws {
let wallet = try Wallet(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
guard let transaction = wallet.transactions().first?.transaction else {
print("No transactions found")
return
}
print("First transaction:")
print("Txid: \(transaction.computeTxid())")
print("Version: \(transaction.version())")
print("Total size: \(transaction.totalSize())")
print("Vsize: \(transaction.vsize())")
print("Weight: \(transaction.weight())")
print("Coinbase transaction: \(transaction.isCoinbase())")
print("Is explicitly RBF: \(transaction.isExplicitlyRbf())")
print("Inputs: \(transaction.input())")
print("Outputs: \(transaction.output())")
}
}

View File

@ -1,14 +1,25 @@
import XCTest import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTxBuilderTests: XCTestCase { final class LiveTxBuilderTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
var dbFilePath: URL! var dbFilePath: URL!
override func setUpWithError() throws { override func setUpWithError() throws {
super.setUp() super.setUp()
let fileManager = FileManager.default let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).db" let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).sqlite"
dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName) dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName)
if fileManager.fileExists(atPath: dbFilePath.path) { if fileManager.fileExists(atPath: dbFilePath.path) {
@ -24,30 +35,31 @@ final class LiveTxBuilderTests: XCTestCase {
} }
func testTxBuilder() throws { func testTxBuilder() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet
)
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path, network: .signet
network: .testnet
) )
let esploraClient = EsploraClient(url: "https://esplora.testnet.kuutamo.cloud/") let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan( let update = try esploraClient.fullScan(
wallet: wallet, fullScanRequest: fullScanRequest,
stopGap: 10, stopGap: 10,
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: PartiallySignedTransaction = try TxBuilder() let psbt: Psbt = try TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) .addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0)) .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet) .finish(wallet: wallet)
print(psbt.serialize()) print(psbt.serialize())
@ -57,43 +69,48 @@ final class LiveTxBuilderTests: XCTestCase {
func testComplexTxBuilder() throws { func testComplexTxBuilder() throws {
let descriptor = try Descriptor( let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet network: Network.signet
) )
let changeDescriptor = try Descriptor( let changeDescriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.testnet network: Network.signet
) )
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: changeDescriptor, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path, network: .signet
network: .testnet
) )
let esploraClient = EsploraClient(url: "https://esplora.testnet.kuutamo.cloud/") let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan( let update = try esploraClient.fullScan(
wallet: wallet, fullScanRequest: fullScanRequest,
stopGap: 10, stopGap: 10,
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .testnet) let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet)
let allRecipients: [ScriptAmount] = [ let allRecipients: [ScriptAmount] = [
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200), ScriptAmount(script: recipient1.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200)),
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200) ScriptAmount(script: recipient2.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
] ]
let psbt: PartiallySignedTransaction = try TxBuilder() let psbt: Psbt = try TxBuilder()
.setRecipients(recipients: allRecipients) .setRecipients(recipients: allRecipients)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 4.0)) .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 4))
.changePolicy(changePolicy: ChangeSpendPolicy.changeForbidden) .changePolicy(changePolicy: ChangeSpendPolicy.changeForbidden)
.enableRbf() .enableRbf()
.finish(wallet: wallet) .finish(wallet: wallet)
try! wallet.sign(psbt: psbt) let _ = try! wallet.sign(psbt: psbt)
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
} }

View File

@ -1,14 +1,25 @@
import XCTest import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveWalletTests: XCTestCase { final class LiveWalletTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
var dbFilePath: URL! var dbFilePath: URL!
override func setUpWithError() throws { override func setUpWithError() throws {
super.setUp() super.setUp()
let fileManager = FileManager.default let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).db" let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).sqlite"
dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName) dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName)
if fileManager.fileExists(atPath: dbFilePath.path) { if fileManager.fileExists(atPath: dbFilePath.path) {
@ -24,64 +35,66 @@ final class LiveWalletTests: XCTestCase {
} }
func testSyncedBalance() throws { func testSyncedBalance() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet
)
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path, network: .signet
network: .testnet
) )
let esploraClient = EsploraClient(url: "https://esplora.testnet.kuutamo.cloud/") let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan( let update = try esploraClient.fullScan(
wallet: wallet, fullScanRequest: fullScanRequest,
stopGap: 10, stopGap: 10,
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0)) XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)") print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3) let transactions = wallet.transactions().prefix(3)
for tx in transactions { for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx) let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.txid())") print("Transaction: \(tx.transaction.computeTxid())")
print("Sent \(sentAndReceived.sent)") print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received)") print("Received \(sentAndReceived.received.toSat())")
} }
} }
func testBroadcastTransaction() throws { func testBroadcastTransaction() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet
)
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path, network: .signet
network: .testnet
) )
let esploraClient = EsploraClient(url: "https://esplora.testnet.kuutamo.cloud/") let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan( let update = try esploraClient.fullScan(
wallet: wallet, fullScanRequest: fullScanRequest,
stopGap: 10, stopGap: 10,
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) try wallet.applyUpdate(update: update)
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
XCTAssertGreaterThan(
wallet.balance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") print("Balance: \(wallet.balance().total)")
print("Balance: \(wallet.getBalance().total)") let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: PartiallySignedTransaction = try
TxBuilder() TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) .addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0)) .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet) .finish(wallet: wallet)
print(psbt.serialize()) print(psbt.serialize())
@ -90,12 +103,12 @@ final class LiveWalletTests: XCTestCase {
let walletDidSign: Bool = try wallet.sign(psbt: psbt) let walletDidSign: Bool = try wallet.sign(psbt: psbt)
XCTAssertTrue(walletDidSign, "Wallet did not sign transaction") XCTAssertTrue(walletDidSign, "Wallet did not sign transaction")
let tx: Transaction = psbt.extractTx() let tx: Transaction = try! psbt.extractTx()
print(tx.txid()) print(tx.computeTxid())
let fee: UInt64 = try wallet.calculateFee(tx: tx) let fee: Amount = try wallet.calculateFee(tx: tx)
print("Transaction Fee: \(fee)") print("Transaction Fee: \(fee)")
let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx) let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx)
print("Transaction Fee Rate: \(feeRate.asSatPerVb()) sat/vB") print("Transaction Fee Rate: \(feeRate.toSatPerVbCeil()) sat/vB")
try esploraClient.broadcast(transaction: tx) try esploraClient.broadcast(transaction: tx)
} }

View File

@ -15,6 +15,6 @@ final class OfflineDescriptorTests: XCTestCase {
network: Network.testnet network: Network.testnet
) )
XCTAssertEqual(descriptor.asString(), "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx") XCTAssertEqual(descriptor.description, "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx")
} }
} }

View File

@ -0,0 +1,43 @@
import XCTest
@testable import BitcoinDevKit
final class OfflinePersistenceTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
var dbFilePath: URL!
override func setUpWithError() throws {
super.setUp()
guard let resourceUrl = Bundle.module.url(
forResource: "pre_existing_wallet_persistence_test",
withExtension: "sqlite"
) else {
print("error finding resourceURL")
return
}
dbFilePath = resourceUrl
}
func testPersistence() throws {
let sqliteStore = try! SqliteStore(path: dbFilePath.path)
let initialChangeSet = try! sqliteStore.read()
let wallet = try Wallet.newOrLoad(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
changeSet: initialChangeSet,
network: .signet
)
let nextAddress: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external)
print("Address: \(nextAddress)")
XCTAssertTrue(nextAddress.address.description == "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2")
XCTAssertTrue(nextAddress.index == 7)
}
}

View File

@ -2,13 +2,21 @@ import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
final class OfflineWalletTests: XCTestCase { final class OfflineWalletTests: XCTestCase {
private let descriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
private let changeDescriptor = try! Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
var dbFilePath: URL! var dbFilePath: URL!
override func setUpWithError() throws { override func setUpWithError() throws {
super.setUp() super.setUp()
let fileManager = FileManager.default let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).db" let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).sqlite"
dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName) dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName)
if fileManager.fileExists(atPath: dbFilePath.path) { if fileManager.fileExists(atPath: dbFilePath.path) {
@ -24,17 +32,12 @@ final class OfflineWalletTests: XCTestCase {
} }
func testNewAddress() throws { func testNewAddress() throws {
let descriptor: Descriptor = try Descriptor(
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet
)
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path,
network: .testnet network: .testnet
) )
let addressInfo: AddressInfo = wallet.getAddress(addressIndex: AddressIndex.new) let addressInfo: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external)
XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet), XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet),
"Address is not valid for testnet network") "Address is not valid for testnet network")
@ -45,21 +48,16 @@ final class OfflineWalletTests: XCTestCase {
XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin), XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin),
"Address is valid for bitcoin network, but it shouldn't be") "Address is valid for bitcoin network, but it shouldn't be")
XCTAssertEqual(addressInfo.address.asString(), "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e") XCTAssertEqual(addressInfo.address.description, "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989")
} }
func testBalance() throws { func testBalance() throws {
let descriptor: Descriptor = try Descriptor(
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet
)
let wallet = try Wallet( let wallet = try Wallet(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path,
network: .testnet network: .testnet
) )
XCTAssertEqual(wallet.getBalance().total, 0) XCTAssertEqual(wallet.balance().total.toSat(), 0)
} }
} }

View File

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>LibraryIdentifier</key>
<string>macos-arm64_x86_64</string>
<key>LibraryPath</key>
<string>bdkFFI.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>bdkFFI.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>bdkFFI.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -1,4 +0,0 @@
// This is the "umbrella header" for our combined Rust code library.
// It needs to import all of the individual headers.
#import "bdkFFI.h"

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.bitcoindevkit.bdkFFI</string>
<key>CFBundleName</key>
<string>bdkFFI</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleExecutable</key>
<string>bdkFFI</string>
<key>MinimumOSVersion</key>
<string>100</string>
</dict>
</plist>

View File

@ -1,6 +0,0 @@
framework module bdkFFI {
umbrella header "bdkFFI-umbrella.h"
export *
module * { export * }
}

View File

@ -1,4 +0,0 @@
// This is the "umbrella header" for our combined Rust code library.
// It needs to import all of the individual headers.
#import "bdkFFI.h"

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.bitcoindevkit.bdkFFI</string>
<key>CFBundleName</key>
<string>bdkFFI</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleExecutable</key>
<string>bdkFFI</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict>
</plist>

View File

@ -1,6 +0,0 @@
framework module bdkFFI {
umbrella header "bdkFFI-umbrella.h"
export *
module * { export * }
}

View File

@ -1,4 +0,0 @@
// This is the "umbrella header" for our combined Rust code library.
// It needs to import all of the individual headers.
#import "bdkFFI.h"

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.bitcoindevkit.bdkFFI</string>
<key>CFBundleName</key>
<string>bdkFFI</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleExecutable</key>
<string>bdkFFI</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -1,6 +0,0 @@
framework module bdkFFI {
umbrella header "bdkFFI-umbrella.h"
export *
module * { export * }
}

View File

@ -1,43 +0,0 @@
#!/bin/bash
# This script builds local swift-bdk Swift language bindings and corresponding bdkFFI.xcframework.
# The results of this script can be used for locally testing your SPM package adding a local package
# to your application pointing at the bdk-swift directory.
# Run the script from the repo root directory, ie: ./bdk-swift/build-local-swift.sh
rustup install 1.73.0
rustup component add rust-src
rustup target add aarch64-apple-ios # iOS ARM64
rustup target add x86_64-apple-ios # iOS x86_64
rustup target add aarch64-apple-ios-sim # simulator mac M1
rustup target add aarch64-apple-darwin # mac M1
rustup target add x86_64-apple-darwin # mac x86_64
pushd bdk-ffi
mkdir -p Sources/BitcoinDevKit
cargo run --bin uniffi-bindgen generate src/bdk.udl --language swift --out-dir ../bdk-swift/Sources/BitcoinDevKit --no-format
cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-darwin
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-darwin
cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-ios
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios-sim
mkdir -p target/lipo-ios-sim/release-smaller
lipo target/aarch64-apple-ios-sim/release-smaller/libbdkffi.a target/x86_64-apple-ios/release-smaller/libbdkffi.a -create -output target/lipo-ios-sim/release-smaller/libbdkffi.a
mkdir -p target/lipo-macos/release-smaller
lipo target/aarch64-apple-darwin/release-smaller/libbdkffi.a target/x86_64-apple-darwin/release-smaller/libbdkffi.a -create -output target/lipo-macos/release-smaller/libbdkffi.a
popd
pushd bdk-swift
mv Sources/BitcoinDevKit/bdk.swift Sources/BitcoinDevKit/BitcoinDevKit.swift
cp Sources/BitcoinDevKit/bdkFFI.h bdkFFI.xcframework/ios-arm64/bdkFFI.framework/Headers
cp Sources/BitcoinDevKit/bdkFFI.h bdkFFI.xcframework/ios-arm64_x86_64-simulator/bdkFFI.framework/Headers
cp Sources/BitcoinDevKit/bdkFFI.h bdkFFI.xcframework/macos-arm64_x86_64/bdkFFI.framework/Headers
cp ../bdk-ffi/target/aarch64-apple-ios/release-smaller/libbdkffi.a bdkFFI.xcframework/ios-arm64/bdkFFI.framework/bdkFFI
cp ../bdk-ffi/target/lipo-ios-sim/release-smaller/libbdkffi.a bdkFFI.xcframework/ios-arm64_x86_64-simulator/bdkFFI.framework/bdkFFI
cp ../bdk-ffi/target/lipo-macos/release-smaller/libbdkffi.a bdkFFI.xcframework/macos-arm64_x86_64/bdkFFI.framework/bdkFFI
rm Sources/BitcoinDevKit/bdkFFI.h
rm Sources/BitcoinDevKit/bdkFFI.modulemap
#rm bdkFFI.xcframework.zip || true
#zip -9 -r bdkFFI.xcframework.zip bdkFFI.xcframework

60
bdk-swift/build-xcframework.sh Executable file
View File

@ -0,0 +1,60 @@
#!/bin/bash
# This script builds local swift-bdk Swift language bindings and corresponding bdkFFI.xcframework.
# The results of this script can be used for locally testing your SPM package adding a local package
# to your application pointing at the bdk-swift directory.
HEADERPATH="Sources/BitcoinDevKit/BitcoinDevKitFFI.h"
MODMAPPATH="Sources/BitcoinDevKit/BitcoinDevKitFFI.modulemap"
TARGETDIR="../bdk-ffi/target"
OUTDIR="."
RELDIR="release-smaller"
NAME="bdkffi"
STATIC_LIB_NAME="lib${NAME}.a"
NEW_HEADER_DIR="../bdk-ffi/target/include"
# set required rust version and install component and targets
rustup default 1.77.1
rustup component add rust-src
rustup target add aarch64-apple-ios # iOS arm64
rustup target add x86_64-apple-ios # iOS x86_64
rustup target add aarch64-apple-ios-sim # simulator mac M1
rustup target add aarch64-apple-darwin # mac M1
rustup target add x86_64-apple-darwin # mac x86_64
cd ../bdk-ffi/ || exit
# build bdk-ffi rust lib for apple targets
cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-darwin
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-darwin
cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-ios
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios
cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios-sim
# build bdk-ffi Swift bindings and put in bdk-swift Sources
cargo run --bin uniffi-bindgen generate --library ./target/aarch64-apple-ios/release-smaller/libbdkffi.dylib --language swift --out-dir ../bdk-swift/Sources/BitcoinDevKit --no-format
# combine bdk-ffi static libs for aarch64 and x86_64 targets via lipo tool
mkdir -p target/lipo-ios-sim/release-smaller
lipo target/aarch64-apple-ios-sim/release-smaller/libbdkffi.a target/x86_64-apple-ios/release-smaller/libbdkffi.a -create -output target/lipo-ios-sim/release-smaller/libbdkffi.a
mkdir -p target/lipo-macos/release-smaller
lipo target/aarch64-apple-darwin/release-smaller/libbdkffi.a target/x86_64-apple-darwin/release-smaller/libbdkffi.a -create -output target/lipo-macos/release-smaller/libbdkffi.a
cd ../bdk-swift/ || exit
# move bdk-ffi static lib header files to temporary directory
mkdir -p "${NEW_HEADER_DIR}"
mv "${HEADERPATH}" "${NEW_HEADER_DIR}"
mv "${MODMAPPATH}" "${NEW_HEADER_DIR}/module.modulemap"
# remove old xcframework directory
rm -rf "${OUTDIR}/${NAME}.xcframework"
# create new xcframework directory from bdk-ffi static libs and headers
xcodebuild -create-xcframework \
-library "${TARGETDIR}/lipo-macos/${RELDIR}/${STATIC_LIB_NAME}" \
-headers "${NEW_HEADER_DIR}" \
-library "${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" \
-headers "${NEW_HEADER_DIR}" \
-library "${TARGETDIR}/lipo-ios-sim/${RELDIR}/${STATIC_LIB_NAME}" \
-headers "${NEW_HEADER_DIR}" \
-output "${OUTDIR}/${NAME}.xcframework"

14
bdk-swift/justfile Normal file
View File

@ -0,0 +1,14 @@
default:
just --list
build:
bash ./build-xcframework.sh
clean:
rm -rf ../bdk-ffi/target/
test:
swift test
test-offline:
swift test --skip LiveElectrumClientTests --skip LiveMemoryWalletTests --skip LiveTransactionTests --skip LiveTxBuilderTests --skip LiveWalletTests