Compare commits

..

2 Commits

Author SHA1 Message Date
thunderbiscuit
77463fa629 chore: add bdk team author and email to libraries 2023-11-20 12:19:27 -05:00
thunderbiscuit
9df6f6dbc1 chore: update libraries to official release versions 2023-11-20 10:37:47 -05:00
83 changed files with 2446 additions and 4618 deletions

87
.github/ISSUE_TEMPLATE/minor_release.md vendored Normal file
View File

@@ -0,0 +1,87 @@
---
name: Minor Release
about: Create a new minor release [for release managers only]
title: 'Release MAJOR.MINOR+1.0'
labels: 'release'
assignees: ''
---
## Create a new minor release
## 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.
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
#### _Android_
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.
5. - [ ] Build the library and run the tests, and adjust if necessary.
```shell
# start an emulator prior to running the tests
cd ./bdk-android/
./gradlew buildAndroidLib
./gradlew connectedAndroidTest
```
6. - [ ] Update the readme if necessary
#### _JVM_
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
9. - [ ] Build the library and run the tests, and adjust if necessary
```shell
cd ./bdk-jvm/
./gradlew buildJvmLib
./gradlew test
```
10. - [ ] Update the readme if necessary
#### _Swift_
11. - [ ] Run the tests and adjust if necessary
```shell
./bdk-swift/build-local-swift.sh
cd ./bdk-swift/
swift test
```
12. - [ ] Update the readme if necessary
#### _Python_
13. - [ ] Delete the `.tox`, `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
```shell
cd ./bdk-python/
pip3 install --requirement requirements.txt
bash ./scripts/generate-macos-arm64.sh # run the script for your particular platform
python3 setup.py --verbose bdist_wheel
```
15. - [ ] Run the tests and adjust if necessary
```shell
pip3 install ./dist/bdkpython-<yourversion>-py3-none-any.whl --force-reinstall
python -m unittest --verbose tests/test_bdk.py
```
16. - [ ] Update the readme and `setup.py` if necessary
### 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. - [ ] Update bdk-android version from `SNAPSHOT` version to release version
20. - [ ] Update bdk-jvm version from `SNAPSHOT` version to release version
21. - [ ] 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. - [ ] 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.
```shell
git tag v0.6.0 --sign --edit
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. - [ ] 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. - [ ] 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. - [ ] 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. - [ ] Build and publish API docs for JVM, Android, and Java on the website
```shell
./gradlew dokkaHtml # bdk-jvm (Dokka)
./gradlew dokkaJavadoc # bdk-jvm (java-style documentation)
./gradlew dokkaHtml # bdk-android (Dokka)
```
33. - [ ] Post in the announcement channel
34. - [ ] Tweet about the library

View File

@@ -1,95 +0,0 @@
---
name: Release
about: Create a new release [for release managers only]
title: 'Release MAJOR.MINOR.PATCH'
labels: 'release'
assignees: ''
---
# Part 1: Bump 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.
2. - [ ] Once the new bdk release is out, update the PR to replace the release candidate with the full release and merge.
# Part 2: Prepare Libraries for Release Branch
### _Android_
3. - [ ] Update the API docs to reflect the changes in the API
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 offline and live tests, and adjust them if necessary (note that you'll need an Android emulator running).
```shell
# start an emulator prior to running the tests
cd ./bdk-android/
just clean
just build
just test
```
6. - [ ] Update the readme if necessary
### _JVM_
7. - [ ] Update the API docs to reflect the changes in the API
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
```shell
cd ./bdk-jvm/
just clean
just build
just test
```
10. - [ ] Update the readme if necessary
### _Swift_
11. - [ ] Delete the `target` directory in bdk-ffi
12. - [ ] Run the tests and adjust if necessary
```shell
cd ./bdk-swift/
just clean
just build
just test
```
13. - [ ] Update the readme if necessary
### _Python_
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
cd ./bdk-python/
just clean
pip3 install --requirement requirements.txt
bash ./scripts/generate-macos-arm64.sh # run the script for your particular platform
python3 setup.py --verbose bdist_wheel
```
16. - [ ] Run the tests and adjust if necessary
```shell
pip3 install ./dist/bdkpython-<yourversion>-py3-none-any.whl --force-reinstall
python -m unittest --verbose
```
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).
## Part 3: Release Workflow
19. - [ ] Create a new branch off of `master` called `release/<feature version>`, e.g. `release/0.31`
20. - [ ] Update bdk-android version from `SNAPSHOT` version to release version
21. - [ ] Update bdk-jvm version from `SNAPSHOT` version to release version
22. - [ ] Update bdk-python version from `.dev` version to release version
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).
24. - [ ] Get a review and ACK and merge the PR updating all the languages to their release versions
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
git tag v0.6.0 --sign --edit
git push upstream v0.6.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`).
27. - [ ] Make sure the released libraries work and contain the artifacts you would expect
28. - [ ] Aggregate all the changelog notices from the PRs and add them to the changelog file
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`
30. - [ ] Apply changes to the minor_release and patch_release issue templates if they need any
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.
32. - [ ] Make release on GitHub (set as pre-release and generate auto release notes between the previous tag and the new one)
33. - [ ] Post in the announcement channel
34. - [ ] Tweet about the library

View File

@@ -17,7 +17,7 @@ jobs:
strategy: strategy:
matrix: matrix:
rust: rust:
- version: 1.77.1 - version: 1.73.0
clippy: true clippy: true
steps: steps:
- name: "Checkout" - name: "Checkout"
@@ -56,7 +56,7 @@ jobs:
run: cargo clippy --all-targets --features "uniffi/bindgen-tests" -- -D warnings run: cargo clippy --all-targets --features "uniffi/bindgen-tests" -- -D warnings
- name: "Test" - name: "Test"
run: CLASSPATH=./tests/jna/jna-5.14.0.jar cargo test --features uniffi/bindgen-tests run: CLASSPATH=./tests/jna/jna-5.8.0.jar cargo test --features uniffi/bindgen-tests
fmt: fmt:
name: "Rust fmt" name: "Rust fmt"

View File

@@ -27,8 +27,8 @@ jobs:
distribution: temurin distribution: temurin
java-version: 11 java-version: 11
- name: "Set default Rust version to 1.77.1" - name: "Set default Rust version to 1.73.0"
run: rustup default 1.77.1 run: rustup default 1.73.0
- name: "Build bdk-jvm library" - name: "Build bdk-jvm library"
run: | run: |
@@ -48,8 +48,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: "Build Swift package" - name: "Build Swift package"
working-directory: bdk-swift run: bash ./bdk-swift/build-local-swift.sh
run: bash ./build-local-swift.sh
- name: "Run live Swift tests" - name: "Run live Swift tests"
working-directory: bdk-swift working-directory: bdk-swift
@@ -75,11 +74,9 @@ 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: 1.77.1 toolchain: stable
- 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: 17 java-version: 11
- name: "Set default Rust version to 1.77.1" - name: "Set default Rust version to 1.73.0"
run: rustup default 1.77.1 run: rustup default 1.73.0
- 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 Central" - name: "Publish to Maven Local and 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: 17 java-version: 11
- name: "Set default Rust version to 1.77.1" - name: "Set default Rust version to 1.73.0"
run: rustup default 1.77.1 run: rustup default 1.73.0
- 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: 17 java-version: 11
- name: "Set default Rust version to 1.77.1" - name: "Set default Rust version to 1.73.0"
run: rustup default 1.77.1 run: rustup default 1.73.0
- 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: 17 java-version: 11
- name: "Set default Rust version to 1.77.1" - name: "Set default Rust version to 1.73.0"
run: rustup default 1.77.1 run: rustup default 1.73.0
- name: "Build bdk-jvm library" - name: "Build bdk-jvm library"
run: | run: |

View File

@@ -24,18 +24,15 @@ 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
- name: "Install Rust 1.77.1" - uses: actions-rs/toolchain@v1
uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.77.1 toolchain: stable
- name: "Generate bdk.py and binaries" - name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh run: bash ./scripts/generate-linux.sh
@@ -62,8 +59,6 @@ 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
@@ -101,8 +96,6 @@ 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
@@ -139,8 +132,6 @@ 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

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

View File

@@ -33,18 +33,14 @@ 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
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.77.1 toolchain: stable
- name: "Generate bdk.py and binaries" - name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh run: bash ./scripts/generate-linux.sh
@@ -78,8 +74,6 @@ 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
@@ -123,8 +117,6 @@ 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
@@ -166,8 +158,6 @@ 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,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: "Build Swift package" - name: "Build Swift package"
working-directory: bdk-swift run: bash ./bdk-swift/build-local-swift.sh
run: bash ./build-local-swift.sh
- name: "Run Swift tests" - name: "Run Swift tests"
working-directory: bdk-swift working-directory: bdk-swift

View File

@@ -1,30 +1,8 @@
# Changelog # Changelog
Changelog information can also be found in each release's git tag (which can be viewed with `git tag -ln100 "v*"`), as well as on the [GitHub releases](https://github.com/bitcoindevkit/bdk-ffi/releases) page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. Changelog information can also be found in each release's git tag (which can be viewed with `git tag -ln100 "v*"`), as well as on the [GitHub releases](https://github.com/bitcoindevkit/bdk-ffi/releases) page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
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.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.
## [1.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:
- Create and recover wallets using descriptors, including the four descriptor templates
- Sync a wallet using a blocking Esplora client
- Query the wallet for balance and addresses
- Create and sign transactions using the transaction builder
- Broadcast transactions
## [0.31.0]
This release updates the bindings libraries to bdk version 0.29.0, updating rust-bitcoin to version 0.30.2.
- APIs Changed:
- `BumpFeeTxBuilder.allow_shrinking()` now takes a `Script` as its argument [#443]
- The `Address` constructor now takes a `Network` argument [#443]
- The `Payload::PubkeyHash` and `Payload::ScriptHash` now have string arguments instead of byte arrays [#443]
- APIs Added:
- The `Address` type now has the `is_valid_for_network()` method [#443]
[#443]: https://github.com/bitcoindevkit/bdk-ffi/pull/443
## [0.30.0] ## [0.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.
@@ -245,9 +223,6 @@ 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
[1.0.0-alpha.7]: https://github.com/bitcoindevkit/bdk-ffi/compare/v1.0.0-alpha.2a...v1.0.0-alpha.7
[1.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.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
[v0.28.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.27.1...v0.28.0 [v0.28.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.27.1...v0.28.0

View File

@@ -8,7 +8,7 @@
</p> </p>
## 🚨 Warning 🚨 ## 🚨 Warning 🚨
The `master` branch of this repository is being migrated to the [bdk 1.0 API](https://github.com/bitcoindevkit/bdk) and is incomplete. For production-ready libraries, use the [`0.31.X`](https://github.com/bitcoindevkit/bdk-ffi/tree/release/0.30) releases. The `master` branch of this repository is being migrated to the [bdk 1.0 API](https://github.com/bitcoindevkit/bdk) and is incomplete. For production-ready libraries, use the [`0.30.X`](https://github.com/bitcoindevkit/bdk-ffi/tree/release/0.30) releases.
## Readme ## Readme
The workspace in this repository creates the `libbdkffi` multi-language library for the Rust-based The workspace in this repository creates the `libbdkffi` multi-language library for the Rust-based
@@ -26,21 +26,22 @@ 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] | |
## 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
just build
just offlinetests
just publishlocal
```
## Minimum Supported Rust Version (MSRV) ## Minimum Supported Rust Version (MSRV)
This library should compile with any combination of features with Rust 1.77.1.
This library should compile with any combination of features with Rust 1.73.0.
## 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,6 +13,24 @@ 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 databaseConfig = DatabaseConfig.Memory
val blockchainConfig = BlockchainConfig.Electrum(
ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5u, null, 10u, true)
)
val wallet = Wallet(externalDescriptor, internalDescriptor, Network.TESTNET, databaseConfig, blockchainConfig)
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
@@ -31,41 +49,36 @@ 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.9.23` or later is required to build the library._ _Note that Kotlin version `1.6.10` 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.77.1): 3. Install Rust (note that we are currently building using Rust 1.73.0):
```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.77.1 rustup default 1.73.0
``` ```
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 build tool. Note that currently, NDK version 25.2.9519653 or above is required. For example: 6. Setup `$ANDROID_SDK_ROOT` and `$ANDROID_NDK_ROOT` path variables (which are required by the
build tool), for example (note that currently, NDK version 25.2.9519653 or above is required):
```shell ```shell
# macOS export ANDROID_SDK_ROOT=~/Android/Sdk
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
cd bdk-android cd bdk-android
./gradlew buildAndroidLib ./gradlew buildAndroidLib
``` ```
1. Start android emulator and run tests 8. Start android emulator (must be x86_64) and run tests
```sh ```sh
./gradlew connectedAndroidTest ./gradlew connectedAndroidTest
``` ```
@@ -73,7 +86,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 -P localBuild ./gradlew publishToMavenLocal --exclude-task signMavenPublication
``` ```
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:
@@ -82,7 +95,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 the `localBuild` flag: and use the `publishToMavenLocal` task without excluding the signing task:
```shell ```shell
./gradlew publishToMavenLocal ./gradlew publishToMavenLocal
``` ```

View File

@@ -1,10 +1,14 @@
buildscript {
repositories {
google()
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.2")
}
}
plugins { plugins {
id("com.android.library").version("8.3.1").apply(false) id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
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.10-SNAPSHOT libraryVersion=1.0.0-alpha.2-rc1

Binary file not shown.

View File

@@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.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/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/master/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,11 +80,13 @@ do
esac esac
done done
# This is normally unused APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # 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"'
# 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
@@ -131,29 +133,22 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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
@@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# Collect all arguments for the java command: # * put everything else in single quotes, so that it's not re-expanded.
# * 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" \
@@ -214,12 +205,6 @@ 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,8 +25,7 @@
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%
@@ -41,7 +40,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% equ 0 goto execute if "%ERRORLEVEL%" == "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.
@@ -76,15 +75,13 @@ 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% equ 0 goto mainEnd if "%ERRORLEVEL%"=="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!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 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

View File

@@ -1,20 +0,0 @@
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,21 +5,25 @@ val libraryVersion: String by project
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android") version "1.6.10"
id("org.gradle.maven-publish") id("maven-publish")
id("org.gradle.signing") id("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 {
namespace = "org.bitcoindevkit" compileSdk = 31
compileSdk = 34
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 31
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
} }
@@ -39,22 +43,8 @@ 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.8.0@aar")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
implementation("androidx.appcompat:appcompat:1.4.0") implementation("androidx.appcompat:appcompat:1.4.0")
implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.core:core-ktx:1.7.0")
@@ -106,22 +96,9 @@ 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
@@ -129,7 +106,8 @@ signing {
sign(publishing.publications) sign(publishing.publications)
} }
// This task dependency ensures that we build the bindings binaries before running the tests // This task dependency ensures that we build the bindings
// binaries before running the tests
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
dependsOn("buildAndroidLib") dependsOn("buildAndroidLib")
} }

View File

@@ -1,81 +1,30 @@
package org.bitcoindevkit package org.bitcoindevkit
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test import org.junit.Test
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File
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
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@Test @Test
fun testTxBuilder() { fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET) val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() val update = esploraClient.scan(wallet, 10uL, 1uL)
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() println("Balance: ${wallet.getBalance().total()}")
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL) assert(wallet.getBalance().total() > 0uL)
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: Psbt = TxBuilder() val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(2.0f)
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
} }
@Test
fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL)
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
)
val psbt: Psbt = TxBuilder()
.setRecipients(allRecipients)
.feeRate(FeeRate.fromSatPerVb(4uL))
.changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN)
.enableRbf()
.finish(wallet)
wallet.sign(psbt)
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
}
} }

View File

@@ -2,74 +2,46 @@ package org.bitcoindevkit
import org.junit.Test import org.junit.Test
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File
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
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET) val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() // val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.scan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() println("Balance: ${wallet.getBalance().total()}")
println("Balance: ${wallet.getBalance().total}")
val balance: Balance = wallet.getBalance() val balance: Balance = wallet.getBalance()
println("Balance: $balance") println("Balance: $balance")
assert(wallet.getBalance().total > 0uL) assert(wallet.getBalance().total() > 0uL)
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.txid()}")
println("Sent ${sentAndReceived.sent}")
println("Received ${sentAndReceived.received}")
}
} }
@Test @Test
fun testBroadcastTransaction() { fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET) val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() val update = esploraClient.scan(wallet, 10uL, 1uL)
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address}")
assert(wallet.getBalance().total > 0uL) { wallet.applyUpdate(update)
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." println("Balance: ${wallet.getBalance().total()}")
println("New address: ${wallet.getAddress(AddressIndex.New).address}")
assert(wallet.getBalance().total() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again."
} }
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: Psbt = TxBuilder() val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(4uL)) .feeRate(4.0f)
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@@ -79,14 +51,8 @@ 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.txid()}")
val txFee: ULong = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}")
val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")
esploraClient.broadcast(tx) esploraClient.broadcast(tx)
} }
} }

View File

@@ -4,6 +4,7 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith import org.junit.runner.RunWith
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OfflineDescriptorTest { class OfflineDescriptorTest {

View File

@@ -3,26 +3,11 @@ package org.bitcoindevkit
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.assertFalse
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File
import kotlin.test.AfterTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OfflineWalletTest { class OfflineWalletTest {
private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@Test @Test
fun testDescriptorBip86() { fun testDescriptorBip86() {
val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12) val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12)
@@ -38,18 +23,12 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet.newNoPersist(
descriptor, descriptor,
null, null,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New)
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")
assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest 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 = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e",
@@ -63,16 +42,15 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet.newNoPersist(
descriptor, descriptor,
null, null,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
assertEquals( assertEquals(
expected = 0uL, expected = 0uL,
actual = wallet.getBalance().total actual = wallet.getBalance().total()
) )
} }
} }

View File

@@ -1,3 +1,6 @@
<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,5 +1,6 @@
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,11 +17,6 @@ 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") {
@@ -31,6 +26,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"),
@@ -54,6 +56,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"),
@@ -77,6 +86,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"),
@@ -97,8 +113,6 @@ 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/")
@@ -123,14 +137,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", "--out-dir", "../bdk-android/lib/src/main/kotlin", "--no-format")
executable("cargo") executable("cargo")
args(cargoArgs) args(cargoArgs)

View File

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

929
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.10" version = "1.0.0-alpha.2"
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.10", features = ["all-keys", "keys-bip39"] } bdk = { version = "1.0.0-alpha.2", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.12.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
bdk_file_store = { version = "0.10.0" }
uniffi = { version = "=0.26.1" } # TODO 22: The bdk_esplora crate uses esplora_client which uses reqwest for async. By default it uses the system
bitcoin-internals = { version = "0.2.0", features = ["alloc"] } # openssl library, which is creating problems for cross-compilation. I'd rather use rustls, but it's hidden
thiserror = "1.0.58" # behind a feature flag. We need to look into whether openssl-sys is really required by bdk or if using rustls
# would work just as well. This here is a temporary workaround which removes the async feature on the bdk_esplora crate.
# See PR #1179 https://github.com/bitcoindevkit/bdk/pull/1179 for the fix in bdk.
# bdk = { git = "https://github.com/thunderbiscuit/bdk.git", branch = "test-rust-tls", version = "1.0.0-alpha.2", features = ["all-keys", "keys-bip39"] }
# bdk_esplora = { git = "https://github.com/thunderbiscuit/bdk.git", branch = "test-rust-tls", version = "0.4.0", package = "bdk_esplora", default-features = false, features = ["std", "blocking", "async-https-rustls"] }
bdk_esplora = { version = "0.4.0", default-features = false, features = ["std", "blocking"] }
uniffi = { version = "=0.25.1" }
[build-dependencies] [build-dependencies]
uniffi = { version = "=0.26.1", features = ["build"] } uniffi = { version = "=0.25.1", features = ["build"] }
[dev-dependencies] [dev-dependencies]
uniffi = { version = "=0.26.1", features = ["bindgen-tests"] } uniffi = { version = "=0.25.1", features = ["bindgen-tests"] }
assert_matches = "1.5.0" assert_matches = "1.5.0"
[profile.release-smaller] [profile.release-smaller]

View File

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

View File

@@ -1,194 +1,7 @@
namespace bdk {}; namespace bdk {};
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - error module // bdk crate - root module
// ------------------------------------------------------------------------
[Error]
interface AddressError {
Base58();
Bech32();
WitnessVersion(string error_message);
WitnessProgram(string error_message);
UncompressedPubkey();
ExcessiveScriptSize();
UnrecognizedScript();
NetworkValidation(Network required, Network found, string address);
OtherAddressErr();
};
[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]
interface CalculateFeeError {
MissingTxOut(sequence<OutPoint> out_points);
NegativeFee(i64 fee);
};
[Error]
interface CannotConnectError {
Include(u32 height);
};
[Error]
interface CreateTxError {
Descriptor(string error_message);
Persist(string error_message);
Policy(string error_message);
SpendingPolicyRequired(string kind);
Version0();
Version1Csv();
LockTime(string requested, string required);
RbfSequence();
RbfSequenceCsv(string rbf, string csv);
FeeTooLow(u64 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);
};
[Error]
interface DescriptorKeyError {
Parse(string error_message);
InvalidKeyType();
Bip32(string error_message);
};
[Error]
interface EsploraError {
Minreq(string error_message);
HttpResponse(u16 status, string error_message);
Parsing(string error_message);
StatusCode(string error_message);
BitcoinEncoding(string error_message);
HexToArray(string error_message);
HexToBytes(string error_message);
TransactionNotFound();
HeaderHeightNotFound(u32 height);
HeaderHashNotFound();
InvalidHttpHeaderName(string name);
InvalidHttpHeaderValue(string value);
RequestAlreadyConsumed();
};
[Error]
interface ExtractTxError {
AbsurdFeeRate(u64 fee_rate);
MissingInputValue();
SendingTooMuch();
OtherExtractTxErr();
};
[Error]
enum FeeRateError {
"ArithmeticOverflow"
};
[Error]
interface PersistenceError {
Write(string error_message);
};
[Error]
interface PsbtParseError {
PsbtEncoding(string error_message);
Base64Encoding(string error_message);
};
[Error]
interface SignerError {
MissingKey();
InvalidKey();
UserCanceled();
InputIndexOutOfRange();
MissingNonWitnessUtxo();
InvalidNonWitnessUtxo();
MissingWitnessUtxo();
MissingWitnessScript();
MissingHdKeypath();
NonStandardSighash();
InvalidSighash();
SighashError(string error_message);
MiniscriptPsbt(string error_message);
External(string error_message);
};
[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 {
Io(string error_message);
InvalidMagicBytes(sequence<u8> got, sequence<u8> expected);
Descriptor();
Persist(string error_message);
NotInitialized();
LoadedGenesisDoesNotMatch(string expected, string got);
LoadedNetworkDoesNotMatch(Network expected, Network? got);
};
// ------------------------------------------------------------------------
// bdk crate - types module
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
enum KeychainKind { enum KeychainKind {
@@ -196,105 +9,92 @@ enum KeychainKind {
"Internal", "Internal",
}; };
// ------------------------------------------------------------------------
// bdk crate - wallet module
// ------------------------------------------------------------------------
[Error]
enum BdkError {
"Generic",
"NoRecipients",
"NoUtxosSelected",
"OutputBelowDustLimit",
"InsufficientFunds",
"BnBTotalTriesExceeded",
"BnBNoExactMatch",
"UnknownUtxo",
"TransactionNotFound",
"TransactionConfirmed",
"IrreplaceableTransaction",
"FeeRateTooLow",
"FeeTooLow",
"FeeRateUnavailable",
"MissingKeyOrigin",
"Key",
"ChecksumMismatch",
"SpendingPolicyRequired",
"InvalidPolicyPathError",
"Signer",
"InvalidOutpoint",
"Descriptor",
"Miniscript",
"MiniscriptPsbt",
"Bip32",
"Psbt",
};
enum ChangeSpendPolicy {
"ChangeAllowed",
"OnlyChange",
"ChangeForbidden"
};
interface Balance {
u64 immature();
u64 trusted_pending();
u64 untrusted_pending();
u64 confirmed();
u64 trusted_spendable();
u64 total();
};
dictionary AddressInfo { dictionary AddressInfo {
u32 index; u32 index;
Address address; Address address;
KeychainKind keychain; KeychainKind keychain;
}; };
dictionary Balance {
u64 immature;
u64 trusted_pending;
u64 untrusted_pending;
u64 confirmed;
u64 trusted_spendable;
u64 total;
};
dictionary LocalOutput {
OutPoint outpoint;
TxOut txout;
KeychainKind keychain;
boolean is_spent;
};
dictionary TxOut {
u64 value;
Script script_pubkey;
};
[Enum] [Enum]
interface ChainPosition { interface AddressIndex {
Confirmed(u32 height, u64 timestamp); New();
Unconfirmed(u64 timestamp); LastUnused();
}; Peek(u32 index);
dictionary CanonicalTx {
Transaction transaction;
ChainPosition chain_position;
};
interface FullScanRequest {};
interface SyncRequest {};
// ------------------------------------------------------------------------
// bdk crate - wallet module
// ------------------------------------------------------------------------
enum ChangeSpendPolicy {
"ChangeAllowed",
"OnlyChange",
"ChangeForbidden"
}; };
interface Wallet { interface Wallet {
[Throws=WalletCreationError] [Name=new_no_persist, Throws=BdkError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network); constructor(Descriptor descriptor, Descriptor? change_descriptor, Network network);
[Throws=PersistenceError] AddressInfo get_address(AddressIndex address_index);
AddressInfo reveal_next_address(KeychainKind keychain);
AddressInfo get_internal_address(AddressIndex address_index);
Network network(); Network network();
Balance get_balance(); Balance get_balance();
[Throws=CannotConnectError] boolean is_mine(Script script);
[Throws=BdkError]
void apply_update(Update update); void apply_update(Update update);
[Throws=PersistenceError] [Throws=BdkError]
boolean commit(); boolean sign(PartiallySignedTransaction psbt);
boolean is_mine([ByRef] Script script);
[Throws=SignerError]
boolean sign(Psbt psbt);
SentAndReceivedValues sent_and_received([ByRef] Transaction tx);
sequence<CanonicalTx> transactions();
[Throws=TxidParseError]
CanonicalTx? get_tx(string txid);
[Throws=CalculateFeeError]
u64 calculate_fee([ByRef] Transaction tx);
[Throws=CalculateFeeError]
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();
}; };
interface Update {}; interface Update {};
@@ -302,14 +102,12 @@ interface Update {};
interface TxBuilder { interface TxBuilder {
constructor(); constructor();
TxBuilder add_recipient([ByRef] Script script, u64 amount); TxBuilder add_recipient(Script script, u64 amount);
TxBuilder set_recipients(sequence<ScriptAmount> recipients); TxBuilder set_recipients(sequence<ScriptAmount> script_amount);
TxBuilder add_unspendable(OutPoint unspendable); TxBuilder add_unspendable(OutPoint unspendable);
TxBuilder unspendable(sequence<OutPoint> unspendable);
TxBuilder add_utxo(OutPoint outpoint); TxBuilder add_utxo(OutPoint outpoint);
TxBuilder change_policy(ChangeSpendPolicy change_policy); TxBuilder change_policy(ChangeSpendPolicy change_policy);
@@ -320,31 +118,12 @@ interface TxBuilder {
TxBuilder manually_selected_only(); TxBuilder manually_selected_only();
TxBuilder fee_rate([ByRef] FeeRate fee_rate); TxBuilder fee_rate(float sat_per_vbyte);
TxBuilder fee_absolute(u64 fee);
TxBuilder drain_wallet(); TxBuilder drain_wallet();
TxBuilder drain_to([ByRef] Script script); [Throws=BdkError]
PartiallySignedTransaction finish([ByRef] Wallet wallet);
TxBuilder enable_rbf();
TxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=CreateTxError]
Psbt finish([ByRef] Wallet wallet);
};
interface BumpFeeTxBuilder {
constructor(string txid, FeeRate fee_rate);
BumpFeeTxBuilder enable_rbf();
BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=CreateTxError]
Psbt finish([ByRef] Wallet wallet);
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -354,31 +133,31 @@ interface BumpFeeTxBuilder {
interface Mnemonic { interface Mnemonic {
constructor(WordCount word_count); constructor(WordCount word_count);
[Name=from_string, Throws=Bip39Error] [Name=from_string, Throws=BdkError]
constructor(string mnemonic); constructor(string mnemonic);
[Name=from_entropy, Throws=Bip39Error] [Name=from_entropy, Throws=BdkError]
constructor(sequence<u8> entropy); constructor(sequence<u8> entropy);
string as_string(); string as_string();
}; };
interface DerivationPath { interface DerivationPath {
[Throws=Bip32Error] [Throws=BdkError]
constructor(string path); constructor(string path);
}; };
interface DescriptorSecretKey { interface DescriptorSecretKey {
constructor(Network network, [ByRef] Mnemonic mnemonic, string? password); constructor(Network network, Mnemonic mnemonic, string? password);
[Name=from_string, Throws=DescriptorKeyError] [Name=from_string, Throws=BdkError]
constructor(string secret_key); constructor(string secret_key);
[Throws=DescriptorKeyError] [Throws=BdkError]
DescriptorSecretKey derive([ByRef] DerivationPath path); DescriptorSecretKey derive(DerivationPath path);
[Throws=DescriptorKeyError] [Throws=BdkError]
DescriptorSecretKey extend([ByRef] DerivationPath path); DescriptorSecretKey extend(DerivationPath path);
DescriptorPublicKey as_public(); DescriptorPublicKey as_public();
@@ -388,45 +167,45 @@ interface DescriptorSecretKey {
}; };
interface DescriptorPublicKey { interface DescriptorPublicKey {
[Name=from_string, Throws=DescriptorKeyError] [Name=from_string, Throws=BdkError]
constructor(string public_key); constructor(string public_key);
[Throws=DescriptorKeyError] [Throws=BdkError]
DescriptorPublicKey derive([ByRef] DerivationPath path); DescriptorPublicKey derive(DerivationPath path);
[Throws=DescriptorKeyError] [Throws=BdkError]
DescriptorPublicKey extend([ByRef] DerivationPath path); DescriptorPublicKey extend(DerivationPath path);
string as_string(); string as_string();
}; };
interface Descriptor { interface Descriptor {
[Throws=DescriptorError] [Throws=BdkError]
constructor(string descriptor, Network network); constructor(string descriptor, Network network);
[Name=new_bip44] [Name=new_bip44]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network); constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[Name=new_bip44_public] [Name=new_bip44_public]
constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); constructor(DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network);
[Name=new_bip49] [Name=new_bip49]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network); constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[Name=new_bip49_public] [Name=new_bip49_public]
constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); constructor(DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network);
[Name=new_bip84] [Name=new_bip84]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network); constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[Name=new_bip84_public] [Name=new_bip84_public]
constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); constructor(DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network);
[Name=new_bip86] [Name=new_bip86]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network); constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[Name=new_bip86_public] [Name=new_bip86_public]
constructor([ByRef] DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); constructor(DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network);
string as_string(); string as_string();
@@ -440,14 +219,11 @@ interface Descriptor {
interface EsploraClient { interface EsploraClient {
constructor(string url); constructor(string url);
[Throws=EsploraError] [Throws=BdkError]
Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 parallel_requests); Update scan(Wallet wallet, u64 stop_gap, u64 parallel_requests);
[Throws=EsploraError] [Throws=BdkError]
Update sync(SyncRequest sync_request, u64 parallel_requests); void broadcast(Transaction transaction);
[Throws=EsploraError]
void broadcast([ByRef] Transaction transaction);
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -459,11 +235,6 @@ dictionary ScriptAmount {
u64 amount; u64 amount;
}; };
dictionary SentAndReceivedValues {
u64 sent;
u64 received;
};
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - bitcoin re-exports // bdk crate - bitcoin re-exports
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -474,7 +245,6 @@ interface Script {
sequence<u8> to_bytes(); sequence<u8> to_bytes();
}; };
[NonExhaustive]
enum Network { enum Network {
"Bitcoin", "Bitcoin",
"Testnet", "Testnet",
@@ -491,7 +261,7 @@ enum WordCount {
}; };
interface Address { interface Address {
[Throws=AddressError] [Throws=BdkError]
constructor(string address, Network network); constructor(string address, Network network);
Network network(); Network network();
@@ -501,40 +271,33 @@ interface Address {
string to_qr_uri(); string to_qr_uri();
string as_string(); string as_string();
boolean is_valid_for_network(Network network);
}; };
interface Transaction { interface Transaction {
[Throws=TransactionError] [Throws=BdkError]
constructor(sequence<u8> transaction_bytes); constructor(sequence<u8> transaction_bytes);
string txid(); string txid();
u64 total_size(); u64 size();
u64 vsize(); u64 vsize();
boolean is_coinbase(); boolean is_coin_base();
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();
}; };
interface Psbt { interface PartiallySignedTransaction {
[Throws=PsbtParseError] [Throws=BdkError]
constructor(string psbt_base64); constructor(string psbt_base64);
string serialize(); string serialize();
[Throws=ExtractTxError]
Transaction extract_tx(); Transaction extract_tx();
}; };
@@ -542,17 +305,3 @@ dictionary OutPoint {
string txid; string txid;
u32 vout; u32 vout;
}; };
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();
};

View File

@@ -1,23 +1,19 @@
use crate::error::{AddressError, FeeRateError, PsbtParseError, TransactionError};
use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked};
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut;
use bdk::bitcoin::consensus::encode::serialize;
use bdk::bitcoin::consensus::Decodable; use bdk::bitcoin::consensus::Decodable;
use bdk::bitcoin::psbt::ExtractTxError; use bdk::bitcoin::network::constants::Network as BdkNetwork;
use bdk::bitcoin::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction;
use bdk::bitcoin::Address as BdkAddress; use bdk::bitcoin::Address as BdkAddress;
use bdk::bitcoin::FeeRate as BdkFeeRate;
use bdk::bitcoin::Network;
use bdk::bitcoin::OutPoint as BdkOutPoint; use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::Transaction as BdkTransaction; use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::bitcoin::Txid; use bdk::bitcoin::Txid;
use bdk::Error as BdkError;
use std::io::Cursor; use std::io::Cursor;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
/// A Bitcoin script.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Script(pub(crate) BdkScriptBuf); pub struct Script(pub(crate) BdkScriptBuf);
@@ -38,149 +34,270 @@ impl From<BdkScriptBuf> for Script {
} }
} }
pub enum Network {
/// Mainnet Bitcoin.
Bitcoin,
/// Bitcoin's testnet network.
Testnet,
/// Bitcoin's signet network.
Signet,
/// Bitcoin's regtest network.
Regtest,
}
impl From<Network> for BdkNetwork {
fn from(network: Network) -> Self {
match network {
Network::Bitcoin => BdkNetwork::Bitcoin,
Network::Testnet => BdkNetwork::Testnet,
Network::Signet => BdkNetwork::Signet,
Network::Regtest => BdkNetwork::Regtest,
}
}
}
impl From<BdkNetwork> for Network {
fn from(network: BdkNetwork) -> Self {
match network {
BdkNetwork::Bitcoin => Network::Bitcoin,
BdkNetwork::Testnet => Network::Testnet,
BdkNetwork::Signet => Network::Signet,
BdkNetwork::Regtest => Network::Regtest,
_ => panic!("Network {} not supported", network),
}
}
}
/// A Bitcoin address.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct Address(BdkAddress<NetworkChecked>); pub struct Address {
inner: BdkAddress<NetworkChecked>,
}
impl Address { impl Address {
pub fn new(address: String, network: Network) -> Result<Self, AddressError> { pub fn new(address: String, network: Network) -> Result<Self, BdkError> {
let parsed_address = address.parse::<bdk::bitcoin::Address<NetworkUnchecked>>()?; let parsed_address = address
let network_checked_address = parsed_address.require_network(network)?; .parse::<bdk::bitcoin::Address<NetworkUnchecked>>()
.map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Address(network_checked_address)) let network_checked_address = parsed_address
.require_network(network.into())
.map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Address {
inner: network_checked_address,
})
} }
/// alternative constructor
// fn from_script(script: Arc<Script>, network: Network) -> Result<Self, BdkError> {
// 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 { pub fn network(&self) -> Network {
*self.0.network() self.inner.network.into()
} }
pub fn script_pubkey(&self) -> Arc<Script> { pub fn script_pubkey(&self) -> Arc<Script> {
Arc::new(Script(self.0.script_pubkey())) Arc::new(Script(self.inner.script_pubkey()))
} }
pub fn to_qr_uri(&self) -> String { pub fn to_qr_uri(&self) -> String {
self.0.to_qr_uri() self.inner.to_qr_uri()
} }
pub fn as_string(&self) -> String { pub fn as_string(&self) -> String {
self.0.to_string() self.inner.to_string()
}
pub fn is_valid_for_network(&self, network: Network) -> bool {
let address_str = self.0.to_string();
if let Ok(unchecked_address) = address_str.parse::<BdkAddress<NetworkUnchecked>>() {
unchecked_address.is_valid_for_network(network)
} else {
false
}
} }
} }
impl From<Address> for BdkAddress { impl From<Address> for BdkAddress {
fn from(address: Address) -> Self { fn from(address: Address) -> Self {
address.0 address.inner
} }
} }
impl From<BdkAddress> for Address { impl From<BdkAddress> for Address {
fn from(address: BdkAddress) -> Self { fn from(address: BdkAddress) -> Self {
Address(address) Address { inner: address }
} }
} }
/// A Bitcoin transaction.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transaction(BdkTransaction); pub struct Transaction {
inner: BdkTransaction,
}
impl Transaction { impl Transaction {
pub fn new(transaction_bytes: Vec<u8>) -> Result<Self, TransactionError> { pub fn new(transaction_bytes: Vec<u8>) -> Result<Self, BdkError> {
let mut decoder = Cursor::new(transaction_bytes); let mut decoder = Cursor::new(transaction_bytes);
let tx: BdkTransaction = BdkTransaction::consensus_decode(&mut decoder)?; let tx: BdkTransaction = BdkTransaction::consensus_decode(&mut decoder)
Ok(Transaction(tx)) .map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Transaction { inner: tx })
} }
pub fn txid(&self) -> String { pub fn txid(&self) -> String {
self.0.txid().to_string() self.inner.txid().to_string()
} }
pub fn weight(&self) -> u64 { // fn weight(&self) -> u64 {
self.0.weight().to_wu() // self.inner.weight() as u64
} // }
pub fn total_size(&self) -> u64 { pub fn size(&self) -> u64 {
self.0.total_size() as u64 self.inner.size() as u64
} }
pub fn vsize(&self) -> u64 { pub fn vsize(&self) -> u64 {
self.0.vsize() as u64 self.inner.vsize() as u64
} }
pub fn is_coinbase(&self) -> bool { // fn serialize(&self) -> Vec<u8> {
self.0.is_coinbase() // self.inner.serialize()
// }
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.0.is_explicitly_rbf() self.inner.is_explicitly_rbf()
} }
pub fn is_lock_time_enabled(&self) -> bool { pub fn is_lock_time_enabled(&self) -> bool {
self.0.is_lock_time_enabled() self.inner.is_lock_time_enabled()
} }
pub fn version(&self) -> i32 { pub fn version(&self) -> i32 {
self.0.version.0 self.inner.version
} }
pub fn serialize(&self) -> Vec<u8> { // fn lock_time(&self) -> u32 {
serialize(&self.0) // self.inner.lock_time.0
} // }
// fn input(&self) -> Vec<TxIn> {
// self.inner.input.iter().map(|x| x.into()).collect()
// }
//
// fn output(&self) -> Vec<TxOut> {
// self.inner.output.iter().map(|x| x.into()).collect()
// }
} }
impl From<BdkTransaction> for Transaction { impl From<BdkTransaction> for Transaction {
fn from(tx: BdkTransaction) -> Self { fn from(tx: BdkTransaction) -> Self {
Transaction(tx) Transaction { inner: tx }
} }
} }
impl From<&BdkTransaction> for Transaction { impl From<Transaction> for BdkTransaction {
fn from(tx: &BdkTransaction) -> Self { fn from(tx: Transaction) -> Self {
Transaction(tx.clone()) tx.inner
} }
} }
impl From<&Transaction> for BdkTransaction { pub struct PartiallySignedTransaction {
fn from(tx: &Transaction) -> Self { pub(crate) inner: Mutex<BdkPartiallySignedTransaction>,
tx.0.clone()
}
} }
pub struct Psbt(pub(crate) Mutex<BdkPsbt>); impl PartiallySignedTransaction {
pub(crate) fn new(psbt_base64: String) -> Result<Self, BdkError> {
let psbt: BdkPartiallySignedTransaction =
BdkPartiallySignedTransaction::from_str(&psbt_base64)
.map_err(|e| BdkError::Generic(e.to_string()))?;
impl Psbt { Ok(PartiallySignedTransaction {
pub(crate) fn new(psbt_base64: String) -> Result<Self, PsbtParseError> { inner: Mutex::new(psbt),
let psbt: BdkPsbt = BdkPsbt::from_str(&psbt_base64)?; })
Ok(Psbt(Mutex::new(psbt)))
} }
pub(crate) fn serialize(&self) -> String { pub(crate) fn serialize(&self) -> String {
let psbt = self.0.lock().unwrap().clone(); let psbt = self.inner.lock().unwrap().clone();
psbt.to_string() psbt.to_string()
} }
pub(crate) fn extract_tx(&self) -> Result<Arc<Transaction>, ExtractTxError> { // pub(crate) fn txid(&self) -> String {
let tx: BdkTransaction = self.0.lock().unwrap().clone().extract_tx()?; // let tx = self.inner.lock().unwrap().clone().extract_tx();
let transaction: Transaction = tx.into(); // let txid = tx.txid();
Ok(Arc::new(transaction)) // txid.to_hex()
} // }
}
/// Return the transaction.
impl From<BdkPsbt> for Psbt { pub(crate) fn extract_tx(&self) -> Arc<Transaction> {
fn from(psbt: BdkPsbt) -> Self { let tx = self.inner.lock().unwrap().clone().extract_tx();
Psbt(Mutex::new(psbt)) Arc::new(tx.into())
}
// /// Combines this PartiallySignedTransaction with other PSBT as described by BIP 174.
// ///
// /// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)`
// pub(crate) fn combine(
// &self,
// other: Arc<PartiallySignedTransaction>,
// ) -> Result<Arc<PartiallySignedTransaction>, BdkError> {
// 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.
// /// If the PSBT is missing a TxOut for an input returns None.
// pub(crate) fn fee_amount(&self) -> Option<u64> {
// self.inner.lock().unwrap().fee_amount()
// }
// /// The transaction's fee rate. This value will only be accurate if calculated AFTER the
// /// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
// /// transaction.
// /// 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 {
fn from(psbt: BdkPartiallySignedTransaction) -> Self {
PartiallySignedTransaction {
inner: Mutex::new(psbt),
}
} }
} }
/// A reference to a transaction output.
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct OutPoint { pub struct OutPoint {
/// The referenced transaction's txid.
pub txid: String, pub txid: String,
/// The index of the referenced output in its transaction's vout.
pub vout: u32, pub vout: u32,
} }
@@ -192,407 +309,3 @@ impl From<&OutPoint> for BdkOutPoint {
} }
} }
} }
impl From<&BdkOutPoint> for OutPoint {
fn from(outpoint: &BdkOutPoint) -> Self {
OutPoint {
txid: outpoint.txid.to_string(),
vout: outpoint.vout,
}
}
}
#[derive(Debug, Clone)]
pub struct TxOut {
pub value: u64,
pub script_pubkey: Arc<Script>,
}
impl From<&BdkTxOut> for TxOut {
fn from(tx_out: &BdkTxOut) -> Self {
TxOut {
value: tx_out.value.to_sat(),
script_pubkey: Arc::new(Script(tx_out.script_pubkey.clone())),
}
}
}
#[derive(Clone, Debug)]
pub struct FeeRate(pub 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)]
mod tests {
use crate::bitcoin::Address;
use crate::bitcoin::Network;
#[test]
fn test_is_valid_for_network() {
// ====Docs tests====
// https://docs.rs/bitcoin/0.29.2/src/bitcoin/util/address.rs.html#798-802
let docs_address_testnet_str = "2N83imGV3gPwBzKJQvWJ7cRUY2SpUyU6A5e";
let docs_address_testnet =
Address::new(docs_address_testnet_str.to_string(), Network::Testnet).unwrap();
assert!(
docs_address_testnet.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
docs_address_testnet.is_valid_for_network(Network::Signet),
"Address should be valid for Signet"
);
assert!(
docs_address_testnet.is_valid_for_network(Network::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 =
Address::new(docs_address_mainnet_str.to_string(), Network::Bitcoin).unwrap();
assert!(
docs_address_mainnet.is_valid_for_network(Network::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====
// | Network | Prefix | Address Type |
// |-----------------|---------|--------------|
// | Bitcoin Mainnet | `bc1` | Bech32 |
// | Bitcoin Testnet | `tb1` | Bech32 |
// | Bitcoin Signet | `tb1` | Bech32 |
// | Bitcoin Regtest | `bcrt1` | Bech32 |
// Bech32 - Bitcoin
// Valid for:
// - Bitcoin
// Not valid for:
// - Testnet
// - Signet
// - Regtest
let bitcoin_mainnet_bech32_address_str = "bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj";
let bitcoin_mainnet_bech32_address = Address::new(
bitcoin_mainnet_bech32_address_str.to_string(),
Network::Bitcoin,
)
.unwrap();
assert!(
bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Bitcoin),
"Address should be valid for Bitcoin"
);
assert!(
!bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Testnet),
"Address should not be valid for Testnet"
);
assert!(
!bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Signet),
"Address should not be valid for Signet"
);
assert!(
!bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Regtest),
"Address should not be valid for Regtest"
);
// Bech32 - Testnet
// Valid for:
// - Testnet
// - Regtest
// Not valid for:
// - Bitcoin
// - Regtest
let bitcoin_testnet_bech32_address_str =
"tb1p4nel7wkc34raczk8c4jwk5cf9d47u2284rxn98rsjrs4w3p2sheqvjmfdh";
let bitcoin_testnet_bech32_address = Address::new(
bitcoin_testnet_bech32_address_str.to_string(),
Network::Testnet,
)
.unwrap();
assert!(
!bitcoin_testnet_bech32_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_testnet_bech32_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_testnet_bech32_address.is_valid_for_network(Network::Signet),
"Address should be valid for Signet"
);
assert!(
!bitcoin_testnet_bech32_address.is_valid_for_network(Network::Regtest),
"Address should not not be valid for Regtest"
);
// Bech32 - Signet
// Valid for:
// - Signet
// - Testnet
// Not valid for:
// - Bitcoin
// - Regtest
let bitcoin_signet_bech32_address_str =
"tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk";
let bitcoin_signet_bech32_address = Address::new(
bitcoin_signet_bech32_address_str.to_string(),
Network::Signet,
)
.unwrap();
assert!(
!bitcoin_signet_bech32_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_signet_bech32_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_signet_bech32_address.is_valid_for_network(Network::Signet),
"Address should be valid for Signet"
);
assert!(
!bitcoin_signet_bech32_address.is_valid_for_network(Network::Regtest),
"Address should not not be valid for Regtest"
);
// Bech32 - Regtest
// Valid for:
// - Regtest
// Not valid for:
// - Bitcoin
// - Testnet
// - Signet
let bitcoin_regtest_bech32_address_str = "bcrt1q39c0vrwpgfjkhasu5mfke9wnym45nydfwaeems";
let bitcoin_regtest_bech32_address = Address::new(
bitcoin_regtest_bech32_address_str.to_string(),
Network::Regtest,
)
.unwrap();
assert!(
!bitcoin_regtest_bech32_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
!bitcoin_regtest_bech32_address.is_valid_for_network(Network::Testnet),
"Address should not be valid for Testnet"
);
assert!(
!bitcoin_regtest_bech32_address.is_valid_for_network(Network::Signet),
"Address should not be valid for Signet"
);
assert!(
bitcoin_regtest_bech32_address.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest"
);
// ====P2PKH====
// | Network | Prefix for P2PKH | Prefix for P2SH |
// |------------------------------------|------------------|-----------------|
// | Bitcoin Mainnet | `1` | `3` |
// | Bitcoin Testnet, Regtest, Signet | `m` or `n` | `2` |
// P2PKH - Bitcoin
// Valid for:
// - Bitcoin
// Not valid for:
// - Testnet
// - Regtest
let bitcoin_mainnet_p2pkh_address_str = "1FfmbHfnpaZjKFvyi1okTjJJusN455paPH";
let bitcoin_mainnet_p2pkh_address = Address::new(
bitcoin_mainnet_p2pkh_address_str.to_string(),
Network::Bitcoin,
)
.unwrap();
assert!(
bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Bitcoin),
"Address should be valid for Bitcoin"
);
assert!(
!bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Testnet),
"Address should not be valid for Testnet"
);
assert!(
!bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Regtest),
"Address should not be valid for Regtest"
);
// P2PKH - Testnet
// Valid for:
// - Testnet
// - Regtest
// Not valid for:
// - Bitcoin
let bitcoin_testnet_p2pkh_address_str = "mucFNhKMYoBQYUAEsrFVscQ1YaFQPekBpg";
let bitcoin_testnet_p2pkh_address = Address::new(
bitcoin_testnet_p2pkh_address_str.to_string(),
Network::Testnet,
)
.unwrap();
assert!(
!bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest"
);
// P2PKH - Regtest
// Valid for:
// - Testnet
// - Regtest
// Not valid for:
// - Bitcoin
let bitcoin_regtest_p2pkh_address_str = "msiGFK1PjCk8E6FXeoGkQPTscmcpyBdkgS";
let bitcoin_regtest_p2pkh_address = Address::new(
bitcoin_regtest_p2pkh_address_str.to_string(),
Network::Regtest,
)
.unwrap();
assert!(
!bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest"
);
// ====P2SH====
// | Network | Prefix for P2PKH | Prefix for P2SH |
// |------------------------------------|------------------|-----------------|
// | Bitcoin Mainnet | `1` | `3` |
// | Bitcoin Testnet, Regtest, Signet | `m` or `n` | `2` |
// P2SH - Bitcoin
// Valid for:
// - Bitcoin
// Not valid for:
// - Testnet
// - Regtest
let bitcoin_mainnet_p2sh_address_str = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy";
let bitcoin_mainnet_p2sh_address = Address::new(
bitcoin_mainnet_p2sh_address_str.to_string(),
Network::Bitcoin,
)
.unwrap();
assert!(
bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Bitcoin),
"Address should be valid for Bitcoin"
);
assert!(
!bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Testnet),
"Address should not be valid for Testnet"
);
assert!(
!bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Regtest),
"Address should not be valid for Regtest"
);
// P2SH - Testnet
// Valid for:
// - Testnet
// - Regtest
// Not valid for:
// - Bitcoin
let bitcoin_testnet_p2sh_address_str = "2NFUBBRcTJbYc1D4HSCbJhKZp6YCV4PQFpQ";
let bitcoin_testnet_p2sh_address = Address::new(
bitcoin_testnet_p2sh_address_str.to_string(),
Network::Testnet,
)
.unwrap();
assert!(
!bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest"
);
// P2SH - Regtest
// Valid for:
// - Testnet
// - Regtest
// Not valid for:
// - Bitcoin
let bitcoin_regtest_p2sh_address_str = "2NEb8N5B9jhPUCBchz16BB7bkJk8VCZQjf3";
let bitcoin_regtest_p2sh_address = Address::new(
bitcoin_regtest_p2sh_address_str.to_string(),
Network::Regtest,
)
.unwrap();
assert!(
!bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Bitcoin),
"Address should not be valid for Bitcoin"
);
assert!(
bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Testnet),
"Address should be valid for Testnet"
);
assert!(
bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Regtest),
"Address should be valid for Regtest"
);
}
}

View File

@@ -1,10 +1,9 @@
use crate::error::DescriptorError;
use crate::keys::DescriptorPublicKey; use crate::keys::DescriptorPublicKey;
use crate::keys::DescriptorSecretKey; use crate::keys::DescriptorSecretKey;
use crate::Network;
use bdk::bitcoin::bip32::Fingerprint; use bdk::bitcoin::bip32::Fingerprint;
use bdk::bitcoin::key::Secp256k1; use bdk::bitcoin::key::Secp256k1;
use bdk::bitcoin::Network;
use bdk::descriptor::{ExtendedDescriptor, IntoWalletDescriptor}; use bdk::descriptor::{ExtendedDescriptor, IntoWalletDescriptor};
use bdk::keys::DescriptorPublicKey as BdkDescriptorPublicKey; use bdk::keys::DescriptorPublicKey as BdkDescriptorPublicKey;
use bdk::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap}; use bdk::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap};
@@ -12,9 +11,11 @@ use bdk::template::{
Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public, Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public,
DescriptorTemplate, DescriptorTemplate,
}; };
use bdk::Error as BdkError;
use bdk::KeychainKind; use bdk::KeychainKind;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug)] #[derive(Debug)]
pub struct Descriptor { pub struct Descriptor {
@@ -23,9 +24,10 @@ pub struct Descriptor {
} }
impl Descriptor { impl Descriptor {
pub(crate) fn new(descriptor: String, network: Network) -> Result<Self, DescriptorError> { pub(crate) fn new(descriptor: String, network: Network) -> Result<Self, BdkError> {
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.into())?;
Ok(Self { Ok(Self {
extended_descriptor, extended_descriptor,
key_map, key_map,
@@ -33,11 +35,11 @@ impl Descriptor {
} }
pub(crate) fn new_bip44( pub(crate) fn new_bip44(
secret_key: &DescriptorSecretKey, secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.0; let derivable_key = &secret_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@@ -45,8 +47,9 @@ impl Descriptor {
} }
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) = Bip44(derivable_key, keychain_kind)
Bip44(derivable_key, keychain_kind).build(network).unwrap(); .build(network.into())
.unwrap();
Self { Self {
extended_descriptor, extended_descriptor,
key_map, key_map,
@@ -59,13 +62,13 @@ impl Descriptor {
} }
pub(crate) fn new_bip44_public( pub(crate) fn new_bip44_public(
public_key: &DescriptorPublicKey, public_key: Arc<DescriptorPublicKey>,
fingerprint: String, fingerprint: String,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
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.0; let derivable_key = &public_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@@ -75,7 +78,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) =
Bip44Public(derivable_key, fingerprint, keychain_kind) Bip44Public(derivable_key, fingerprint, keychain_kind)
.build(network) .build(network.into())
.unwrap(); .unwrap();
Self { Self {
@@ -90,11 +93,11 @@ impl Descriptor {
} }
pub(crate) fn new_bip49( pub(crate) fn new_bip49(
secret_key: &DescriptorSecretKey, secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.0; let derivable_key = &secret_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@@ -102,8 +105,9 @@ impl Descriptor {
} }
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) = Bip49(derivable_key, keychain_kind)
Bip49(derivable_key, keychain_kind).build(network).unwrap(); .build(network.into())
.unwrap();
Self { Self {
extended_descriptor, extended_descriptor,
key_map, key_map,
@@ -116,13 +120,13 @@ impl Descriptor {
} }
pub(crate) fn new_bip49_public( pub(crate) fn new_bip49_public(
public_key: &DescriptorPublicKey, public_key: Arc<DescriptorPublicKey>,
fingerprint: String, fingerprint: String,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
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.0; let derivable_key = &public_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@@ -132,7 +136,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) =
Bip49Public(derivable_key, fingerprint, keychain_kind) Bip49Public(derivable_key, fingerprint, keychain_kind)
.build(network) .build(network.into())
.unwrap(); .unwrap();
Self { Self {
@@ -147,11 +151,11 @@ impl Descriptor {
} }
pub(crate) fn new_bip84( pub(crate) fn new_bip84(
secret_key: &DescriptorSecretKey, secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.0; let derivable_key = &secret_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@@ -159,8 +163,9 @@ impl Descriptor {
} }
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) = Bip84(derivable_key, keychain_kind)
Bip84(derivable_key, keychain_kind).build(network).unwrap(); .build(network.into())
.unwrap();
Self { Self {
extended_descriptor, extended_descriptor,
key_map, key_map,
@@ -173,13 +178,13 @@ impl Descriptor {
} }
pub(crate) fn new_bip84_public( pub(crate) fn new_bip84_public(
public_key: &DescriptorPublicKey, public_key: Arc<DescriptorPublicKey>,
fingerprint: String, fingerprint: String,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
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.0; let derivable_key = &public_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@@ -189,7 +194,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) =
Bip84Public(derivable_key, fingerprint, keychain_kind) Bip84Public(derivable_key, fingerprint, keychain_kind)
.build(network) .build(network.into())
.unwrap(); .unwrap();
Self { Self {
@@ -204,11 +209,11 @@ impl Descriptor {
} }
pub(crate) fn new_bip86( pub(crate) fn new_bip86(
secret_key: &DescriptorSecretKey, secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
network: Network, network: Network,
) -> Self { ) -> Self {
let derivable_key = &secret_key.0; let derivable_key = &secret_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
@@ -216,8 +221,9 @@ impl Descriptor {
} }
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) = Bip86(derivable_key, keychain_kind)
Bip86(derivable_key, keychain_kind).build(network).unwrap(); .build(network.into())
.unwrap();
Self { Self {
extended_descriptor, extended_descriptor,
key_map, key_map,
@@ -230,13 +236,13 @@ impl Descriptor {
} }
pub(crate) fn new_bip86_public( pub(crate) fn new_bip86_public(
public_key: &DescriptorPublicKey, public_key: Arc<DescriptorPublicKey>,
fingerprint: String, fingerprint: String,
keychain_kind: KeychainKind, keychain_kind: KeychainKind,
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.0; let derivable_key = &public_key.inner;
match derivable_key { match derivable_key {
BdkDescriptorPublicKey::Single(_) => { BdkDescriptorPublicKey::Single(_) => {
@@ -246,7 +252,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey; let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) = let (extended_descriptor, key_map, _) =
Bip86Public(derivable_key, fingerprint, keychain_kind) Bip86Public(derivable_key, fingerprint, keychain_kind)
.build(network) .build(network.into())
.unwrap(); .unwrap();
Self { Self {
@@ -271,78 +277,91 @@ impl Descriptor {
} }
} }
// // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs.
// // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk`
// // crate.
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::*; use crate::*;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use bdk::descriptor::DescriptorError::Key;
use bdk::keys::KeyError::InvalidNetwork;
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();
DescriptorSecretKey::new(Network::Testnet, &mnemonic, None) DescriptorSecretKey::new(Network::Testnet, Arc::new(mnemonic), None)
} }
#[test] #[test]
fn test_descriptor_templates() { fn test_descriptor_templates() {
let master: DescriptorSecretKey = get_descriptor_secret_key(); let master: Arc<DescriptorSecretKey> = Arc::new(get_descriptor_secret_key());
println!("Master: {:?}", master.as_string()); println!("Master: {:?}", master.as_string());
// tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h // tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h
let handmade_public_44 = master let handmade_public_44 = master
.derive(&DerivationPath::new("m/44h/1h/0h".to_string()).unwrap()) .derive(Arc::new(
DerivationPath::new("m/44h/1h/0h".to_string()).unwrap(),
))
.unwrap() .unwrap()
.as_public(); .as_public();
println!("Public 44: {}", handmade_public_44.as_string()); println!("Public 44: {}", handmade_public_44.as_string());
// Public 44: [d1d04177/44'/1'/0']tpubDCoPjomfTqh1e7o1WgGpQtARWtkueXQAepTeNpWiitS3Sdv8RKJ1yvTrGHcwjDXp2SKyMrTEca4LoN7gEUiGCWboyWe2rz99Kf4jK4m2Zmx/* // Public 44: [d1d04177/44'/1'/0']tpubDCoPjomfTqh1e7o1WgGpQtARWtkueXQAepTeNpWiitS3Sdv8RKJ1yvTrGHcwjDXp2SKyMrTEca4LoN7gEUiGCWboyWe2rz99Kf4jK4m2Zmx/*
let handmade_public_49 = master let handmade_public_49 = master
.derive(&DerivationPath::new("m/49h/1h/0h".to_string()).unwrap()) .derive(Arc::new(
DerivationPath::new("m/49h/1h/0h".to_string()).unwrap(),
))
.unwrap() .unwrap()
.as_public(); .as_public();
println!("Public 49: {}", handmade_public_49.as_string()); println!("Public 49: {}", handmade_public_49.as_string());
// Public 49: [d1d04177/49'/1'/0']tpubDC65ZRvk1NDddHrVAUAZrUPJ772QXzooNYmPywYF9tMyNLYKf5wpKE7ZJvK9kvfG3FV7rCsHBNXy1LVKW95jrmC7c7z4hq7a27aD2sRrAhR/* // Public 49: [d1d04177/49'/1'/0']tpubDC65ZRvk1NDddHrVAUAZrUPJ772QXzooNYmPywYF9tMyNLYKf5wpKE7ZJvK9kvfG3FV7rCsHBNXy1LVKW95jrmC7c7z4hq7a27aD2sRrAhR/*
let handmade_public_84 = master let handmade_public_84 = master
.derive(&DerivationPath::new("m/84h/1h/0h".to_string()).unwrap()) .derive(Arc::new(
DerivationPath::new("m/84h/1h/0h".to_string()).unwrap(),
))
.unwrap() .unwrap()
.as_public(); .as_public();
println!("Public 84: {}", handmade_public_84.as_string()); println!("Public 84: {}", handmade_public_84.as_string());
// Public 84: [d1d04177/84'/1'/0']tpubDDNxbq17egjFk2edjv8oLnzxk52zny9aAYNv9CMqTzA4mQDiQq818sEkNe9Gzmd4QU8558zftqbfoVBDQorG3E4Wq26tB2JeE4KUoahLkx6/* // Public 84: [d1d04177/84'/1'/0']tpubDDNxbq17egjFk2edjv8oLnzxk52zny9aAYNv9CMqTzA4mQDiQq818sEkNe9Gzmd4QU8558zftqbfoVBDQorG3E4Wq26tB2JeE4KUoahLkx6/*
let handmade_public_86 = master let handmade_public_86 = master
.derive(&DerivationPath::new("m/86h/1h/0h".to_string()).unwrap()) .derive(Arc::new(
DerivationPath::new("m/86h/1h/0h".to_string()).unwrap(),
))
.unwrap() .unwrap()
.as_public(); .as_public();
println!("Public 86: {}", handmade_public_86.as_string()); println!("Public 86: {}", handmade_public_86.as_string());
// Public 86: [d1d04177/86'/1'/0']tpubDCJzjbcGbdEfXMWaL6QmgVmuSfXkrue7m2YNoacWwyc7a2XjXaKojRqNEbo41CFL3PyYmKdhwg2fkGpLX4SQCbQjCGxAkWHJTw9WEeenrJb/* // Public 86: [d1d04177/86'/1'/0']tpubDCJzjbcGbdEfXMWaL6QmgVmuSfXkrue7m2YNoacWwyc7a2XjXaKojRqNEbo41CFL3PyYmKdhwg2fkGpLX4SQCbQjCGxAkWHJTw9WEeenrJb/*
let template_private_44 = let template_private_44 =
Descriptor::new_bip44(&master, KeychainKind::External, Network::Testnet); Descriptor::new_bip44(master.clone(), KeychainKind::External, Network::Testnet);
let template_private_49 = let template_private_49 =
Descriptor::new_bip49(&master, KeychainKind::External, Network::Testnet); Descriptor::new_bip49(master.clone(), KeychainKind::External, Network::Testnet);
let template_private_84 = let template_private_84 =
Descriptor::new_bip84(&master, KeychainKind::External, Network::Testnet); Descriptor::new_bip84(master.clone(), KeychainKind::External, Network::Testnet);
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.as_string());
println!("Template 44: {}", template_private_44.as_string()); println!("Template 44: {}", template_private_44.as_string());
println!("Template 84: {}", template_private_84.as_string()); println!("Template 84: {}", template_private_84.as_string());
println!("Template 86: {}", template_private_86.as_string()); println!("Template 86: {}", template_private_86.as_string());
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(),
KeychainKind::External, KeychainKind::External,
Network::Testnet, Network::Testnet,
); );
let template_public_49 = Descriptor::new_bip49_public( let template_public_49 = Descriptor::new_bip49_public(
&handmade_public_49, handmade_public_49,
"d1d04177".to_string(), "d1d04177".to_string(),
KeychainKind::External, KeychainKind::External,
Network::Testnet, Network::Testnet,
); );
let template_public_84 = Descriptor::new_bip84_public( let template_public_84 = Descriptor::new_bip84_public(
&handmade_public_84, handmade_public_84,
"d1d04177".to_string(), "d1d04177".to_string(),
KeychainKind::External, KeychainKind::External,
Network::Testnet, Network::Testnet,
); );
let template_public_86 = Descriptor::new_bip86_public( let template_public_86 = Descriptor::new_bip86_public(
&handmade_public_86, handmade_public_86,
"d1d04177".to_string(), "d1d04177".to_string(),
KeychainKind::External, KeychainKind::External,
Network::Testnet, Network::Testnet,
@@ -390,8 +409,11 @@ mod 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 a DescriptorError::Key with inner InvalidNetwork error // Creating a Descriptor using an extended key that doesn't match the network provided will throw and InvalidNetwork Error
assert!(descriptor1.is_ok()); assert!(descriptor1.is_ok());
assert_matches!(descriptor2.unwrap_err(), DescriptorError::Key { .. }); assert_matches!(
descriptor2.unwrap_err(),
bdk::Error::Descriptor(Key(InvalidNetwork))
)
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,8 @@
use crate::bitcoin::Transaction; use crate::wallet::{Update, Wallet};
use crate::error::EsploraError; use std::ops::Deref;
use crate::types::{FullScanRequest, SyncRequest};
use crate::wallet::Update;
use std::collections::BTreeMap;
use bdk::bitcoin::Transaction as BdkTransaction; use bdk::wallet::Update as BdkUpdate;
use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest; use bdk::Error as BdkError;
use bdk::chain::spk_client::FullScanResult as BdkFullScanResult;
use bdk::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk::chain::spk_client::SyncResult as BdkSyncResult;
use bdk::KeychainKind;
use bdk_esplora::esplora_client::{BlockingClient, Builder}; use bdk_esplora::esplora_client::{BlockingClient, Builder};
use bdk_esplora::EsploraExt; use bdk_esplora::EsploraExt;
@@ -19,65 +12,57 @@ 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(); let client = Builder::new(url.as_str()).build_blocking().unwrap();
Self(client) Self(client)
} }
pub fn full_scan( // This is a temporary solution for scanning. The long-term solution involves not passing
// the wallet to the client at all.
pub fn scan(
&self, &self,
request: Arc<FullScanRequest>, wallet: Arc<Wallet>,
stop_gap: u64, stop_gap: u64,
parallel_requests: u64, parallel_requests: u64,
) -> Result<Arc<Update>, EsploraError> { ) -> Result<Arc<Update>, BdkError> {
// using option and take is not ideal but the only way to take full ownership of the request let wallet = wallet.get_wallet();
let request: BdkFullScanRequest<KeychainKind> = request
let previous_tip = wallet.latest_checkpoint();
let keychain_spks = wallet.spks_of_all_keychains().into_iter().collect();
let (update_graph, last_active_indices) = self
.0 .0
.lock() .scan_txs_with_keychains(
.unwrap() keychain_spks,
.take() None,
.ok_or(EsploraError::RequestAlreadyConsumed)?; None,
stop_gap as usize,
parallel_requests as usize,
)
.unwrap();
let result: BdkFullScanResult<KeychainKind> = let missing_heights = update_graph.missing_heights(wallet.local_chain());
self.0 let chain_update = self
.full_scan(request, stop_gap as usize, parallel_requests as usize)?; .0
.update_local_chain(previous_tip, missing_heights)
.unwrap();
let update = bdk::wallet::Update { let update = BdkUpdate {
last_active_indices: result.last_active_indices, last_active_indices,
graph: result.graph_update, graph: update_graph,
chain: Some(result.chain_update), chain: Some(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)?;
let result: BdkSyncResult = self.0.sync(request, parallel_requests as usize)?; pub fn broadcast(&self, transaction: Arc<crate::bitcoin::Transaction>) -> Result<(), BdkError> {
let bdk_transaction: bdk::bitcoin::Transaction = transaction.deref().clone().into();
let update = bdk::wallet::Update {
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();
self.0 self.0
.broadcast(&bdk_transaction) .broadcast(&bdk_transaction)
.map_err(EsploraError::from) .map_err(|e| BdkError::Generic(e.to_string()))
} }
// pub fn estimate_fee();
} }

View File

@@ -1,10 +1,9 @@
use crate::error::{Bip32Error, Bip39Error, DescriptorKeyError}; use crate::Network;
use bdk::bitcoin::bip32::DerivationPath as BdkDerivationPath; use bdk::bitcoin::bip32::DerivationPath as BdkDerivationPath;
use bdk::bitcoin::key::Secp256k1; use bdk::bitcoin::key::Secp256k1;
use bdk::bitcoin::secp256k1::rand; use bdk::bitcoin::secp256k1::rand;
use bdk::bitcoin::secp256k1::rand::Rng; use bdk::bitcoin::secp256k1::rand::Rng;
use bdk::bitcoin::Network;
use bdk::keys::bip39::WordCount; use bdk::keys::bip39::WordCount;
use bdk::keys::bip39::{Language, Mnemonic as BdkMnemonic}; use bdk::keys::bip39::{Language, Mnemonic as BdkMnemonic};
use bdk::keys::{ use bdk::keys::{
@@ -13,14 +12,20 @@ use bdk::keys::{
}; };
use bdk::miniscript::descriptor::{DescriptorXKey, Wildcard}; use bdk::miniscript::descriptor::{DescriptorXKey, Wildcard};
use bdk::miniscript::BareCtx; use bdk::miniscript::BareCtx;
use bdk::Error as BdkError;
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) BdkMnemonic); /// Mnemonic phrases are a human-readable version of the private keys.
/// Supported number of words are 12, 15, 18, 21 and 24.
pub(crate) struct Mnemonic {
inner: BdkMnemonic,
}
impl Mnemonic { impl Mnemonic {
/// Generates Mnemonic with a random entropy
pub(crate) fn new(word_count: WordCount) -> Self { pub(crate) fn new(word_count: WordCount) -> Self {
// TODO 4: I DON'T KNOW IF THIS IS A DECENT WAY TO GENERATE ENTROPY PLEASE CONFIRM // TODO 4: I DON'T KNOW IF THIS IS A DECENT WAY TO GENERATE ENTROPY PLEASE CONFIRM
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@@ -30,23 +35,27 @@ 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(mnemonic) Mnemonic { inner: mnemonic }
} }
pub(crate) fn from_string(mnemonic: String) -> Result<Self, Bip39Error> { /// Parse a Mnemonic with given string
pub(crate) fn from_string(mnemonic: String) -> Result<Self, BdkError> {
BdkMnemonic::from_str(&mnemonic) BdkMnemonic::from_str(&mnemonic)
.map(Mnemonic) .map(|m| Mnemonic { inner: m })
.map_err(Bip39Error::from) .map_err(|e| BdkError::Generic(e.to_string()))
} }
pub(crate) fn from_entropy(entropy: Vec<u8>) -> Result<Self, Bip39Error> { /// Create a new Mnemonic in the specified language from the given entropy.
/// Entropy must be a multiple of 32 bits (4 bytes) and 128-256 bits in length.
pub(crate) fn from_entropy(entropy: Vec<u8>) -> Result<Self, BdkError> {
BdkMnemonic::from_entropy(entropy.as_slice()) BdkMnemonic::from_entropy(entropy.as_slice())
.map(Mnemonic) .map(|m| Mnemonic { inner: m })
.map_err(Bip39Error::from) .map_err(|e| BdkError::Generic(e.to_string()))
} }
/// Returns Mnemonic as string
pub(crate) fn as_string(&self) -> String { pub(crate) fn as_string(&self) -> String {
self.0.to_string() self.inner.to_string()
} }
} }
@@ -55,48 +64,53 @@ pub(crate) struct DerivationPath {
} }
impl DerivationPath { impl DerivationPath {
pub(crate) fn new(path: String) -> Result<Self, Bip32Error> { pub(crate) fn new(path: String) -> Result<Self, BdkError> {
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(Bip32Error::from) .map_err(|e| BdkError::Generic(e.to_string()))
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct DescriptorSecretKey(pub(crate) BdkDescriptorSecretKey); pub struct DescriptorSecretKey {
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: Arc<Mnemonic>, password: Option<String>) -> Self {
let mnemonic = mnemonic.0.clone(); let mnemonic = mnemonic.inner.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,
xkey: xkey.into_xprv(network).unwrap(), xkey: xkey.into_xprv(network.into()).unwrap(),
derivation_path: BdkDerivationPath::master(), derivation_path: BdkDerivationPath::master(),
wildcard: Wildcard::Unhardened, wildcard: Wildcard::Unhardened,
}); });
Self(descriptor_secret_key) Self {
inner: descriptor_secret_key,
}
} }
pub(crate) fn from_string(private_key: String) -> Result<Self, DescriptorKeyError> { pub(crate) fn from_string(private_key: String) -> Result<Self, BdkError> {
let descriptor_secret_key = BdkDescriptorSecretKey::from_str(private_key.as_str()) let descriptor_secret_key = BdkDescriptorSecretKey::from_str(private_key.as_str())
.map_err(DescriptorKeyError::from)?; .map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Self(descriptor_secret_key)) Ok(Self {
inner: descriptor_secret_key,
})
} }
pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> { pub(crate) fn derive(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor_secret_key = &self.0; let descriptor_secret_key = &self.inner;
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(DescriptorKeyError::InvalidKeyType), BdkDescriptorSecretKey::Single(_) => Err(BdkError::Generic(
"Cannot derive from a single key".to_string(),
)),
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derived_xprv = descriptor_x_key let derived_xprv = descriptor_x_key.xkey.derive_priv(&secp, &path)?;
.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),
@@ -107,17 +121,23 @@ impl DescriptorSecretKey {
derivation_path: BdkDerivationPath::default(), derivation_path: BdkDerivationPath::default(),
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self(derived_descriptor_secret_key))) Ok(Arc::new(Self {
inner: derived_descriptor_secret_key,
}))
} }
BdkDescriptorSecretKey::MultiXPrv(_) => Err(DescriptorKeyError::InvalidKeyType), BdkDescriptorSecretKey::MultiXPrv(_) => Err(BdkError::Generic(
"Cannot derive from a multi key".to_string(),
)),
} }
} }
pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> { pub(crate) fn extend(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let descriptor_secret_key = &self.0; let descriptor_secret_key = &self.inner;
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(DescriptorKeyError::InvalidKeyType), BdkDescriptorSecretKey::Single(_) => Err(BdkError::Generic(
"Cannot extend from a single key".to_string(),
)),
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 {
@@ -126,20 +146,27 @@ impl DescriptorSecretKey {
derivation_path: extended_path, derivation_path: extended_path,
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self(extended_descriptor_secret_key))) Ok(Arc::new(Self {
inner: extended_descriptor_secret_key,
}))
} }
BdkDescriptorSecretKey::MultiXPrv(_) => Err(DescriptorKeyError::InvalidKeyType), BdkDescriptorSecretKey::MultiXPrv(_) => Err(BdkError::Generic(
"Cannot derive from a multi key".to_string(),
)),
} }
} }
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.0.to_public(&secp).unwrap(); let descriptor_public_key = self.inner.to_public(&secp).unwrap();
Arc::new(DescriptorPublicKey(descriptor_public_key)) Arc::new(DescriptorPublicKey {
inner: descriptor_public_key,
})
} }
/// Get the private key as bytes.
pub(crate) fn secret_bytes(&self) -> Vec<u8> { pub(crate) fn secret_bytes(&self) -> Vec<u8> {
let inner = &self.0; let inner = &self.inner;
let secret_bytes: Vec<u8> = match inner { let secret_bytes: Vec<u8> = match inner {
BdkDescriptorSecretKey::Single(_) => { BdkDescriptorSecretKey::Single(_) => {
unreachable!() unreachable!()
@@ -156,32 +183,35 @@ impl DescriptorSecretKey {
} }
pub(crate) fn as_string(&self) -> String { pub(crate) fn as_string(&self) -> String {
self.0.to_string() self.inner.to_string()
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct DescriptorPublicKey(pub(crate) BdkDescriptorPublicKey); pub struct DescriptorPublicKey {
pub(crate) inner: BdkDescriptorPublicKey,
}
impl DescriptorPublicKey { impl DescriptorPublicKey {
pub(crate) fn from_string(public_key: String) -> Result<Self, DescriptorKeyError> { pub(crate) fn from_string(public_key: String) -> Result<Self, BdkError> {
let descriptor_public_key = BdkDescriptorPublicKey::from_str(public_key.as_str()) let descriptor_public_key = BdkDescriptorPublicKey::from_str(public_key.as_str())
.map_err(DescriptorKeyError::from)?; .map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Self(descriptor_public_key)) Ok(Self {
inner: descriptor_public_key,
})
} }
pub(crate) fn derive(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> { pub(crate) fn derive(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor_public_key = &self.0; let descriptor_public_key = &self.inner;
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(DescriptorKeyError::InvalidKeyType), BdkDescriptorPublicKey::Single(_) => Err(BdkError::Generic(
"Cannot derive from a single key".to_string(),
)),
BdkDescriptorPublicKey::XPub(descriptor_x_key) => { BdkDescriptorPublicKey::XPub(descriptor_x_key) => {
let derived_xpub = descriptor_x_key let derived_xpub = descriptor_x_key.xkey.derive_pub(&secp, &path)?;
.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),
@@ -192,17 +222,23 @@ impl DescriptorPublicKey {
derivation_path: BdkDerivationPath::default(), derivation_path: BdkDerivationPath::default(),
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self(derived_descriptor_public_key))) Ok(Arc::new(Self {
inner: derived_descriptor_public_key,
}))
} }
BdkDescriptorPublicKey::MultiXPub(_) => Err(DescriptorKeyError::InvalidKeyType), BdkDescriptorPublicKey::MultiXPub(_) => Err(BdkError::Generic(
"Cannot derive from a multi xpub".to_string(),
)),
} }
} }
pub(crate) fn extend(&self, path: &DerivationPath) -> Result<Arc<Self>, DescriptorKeyError> { pub(crate) fn extend(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let descriptor_public_key = &self.0; let descriptor_public_key = &self.inner;
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(DescriptorKeyError::InvalidKeyType), BdkDescriptorPublicKey::Single(_) => Err(BdkError::Generic(
"Cannot extend from a single key".to_string(),
)),
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 {
@@ -211,60 +247,67 @@ impl DescriptorPublicKey {
derivation_path: extended_path, derivation_path: extended_path,
wildcard: descriptor_x_key.wildcard, wildcard: descriptor_x_key.wildcard,
}); });
Ok(Arc::new(Self(extended_descriptor_public_key))) Ok(Arc::new(Self {
inner: extended_descriptor_public_key,
}))
} }
BdkDescriptorPublicKey::MultiXPub(_) => Err(DescriptorKeyError::InvalidKeyType), BdkDescriptorPublicKey::MultiXPub(_) => Err(BdkError::Generic(
"Cannot derive from a multi xpub".to_string(),
)),
} }
} }
pub(crate) fn as_string(&self) -> String { pub(crate) fn as_string(&self) -> String {
self.0.to_string() self.inner.to_string()
} }
} }
//
// // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs.
// // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk`
// // crate.
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::keys::{DerivationPath, DescriptorPublicKey, DescriptorSecretKey, Mnemonic}; use crate::keys::{DerivationPath, DescriptorPublicKey, DescriptorSecretKey, Mnemonic};
use crate::BdkError;
// use bdk::bitcoin::hashes::hex::ToHex; // use bdk::bitcoin::hashes::hex::ToHex;
use crate::error::DescriptorKeyError;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
use std::sync::Arc; use std::sync::Arc;
fn get_inner() -> DescriptorSecretKey { fn get_inner() -> 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();
DescriptorSecretKey::new(Network::Testnet, &mnemonic, None) DescriptorSecretKey::new(Network::Testnet.into(), Arc::new(mnemonic), None)
} }
fn derive_dsk( fn derive_dsk(
key: &DescriptorSecretKey, key: &DescriptorSecretKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> { ) -> Result<Arc<DescriptorSecretKey>, BdkError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.derive(&path) key.derive(path)
} }
fn extend_dsk( fn extend_dsk(
key: &DescriptorSecretKey, key: &DescriptorSecretKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> { ) -> Result<Arc<DescriptorSecretKey>, BdkError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.extend(&path) key.extend(path)
} }
fn derive_dpk( fn derive_dpk(
key: &DescriptorPublicKey, key: &DescriptorPublicKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> { ) -> Result<Arc<DescriptorPublicKey>, BdkError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.derive(&path) key.derive(path)
} }
fn extend_dpk( fn extend_dpk(
key: &DescriptorPublicKey, key: &DescriptorPublicKey,
path: &str, path: &str,
) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> { ) -> Result<Arc<DescriptorPublicKey>, BdkError> {
let path = DerivationPath::new(path.to_string()).unwrap(); let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.extend(&path) key.extend(path)
} }
#[test] #[test]
@@ -304,7 +347,7 @@ mod test {
assert_eq!(extended_dpk.as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/0/*"); assert_eq!(extended_dpk.as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/0/*");
let wif = "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch"; let wif = "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch";
let extended_key = DescriptorSecretKey::from_string(wif.to_string()).unwrap(); let extended_key = DescriptorSecretKey::from_string(wif.to_string()).unwrap();
let result = extended_key.derive(&DerivationPath::new("m/0".to_string()).unwrap()); let result = extended_key.derive(Arc::new(DerivationPath::new("m/0".to_string()).unwrap()));
dbg!(&result); dbg!(&result);
assert!(result.is_err()); assert!(result.is_err());
} }

View File

@@ -1,58 +1,385 @@
mod bitcoin; mod bitcoin;
mod descriptor; mod descriptor;
mod error;
mod esplora; mod esplora;
mod keys; mod keys;
mod types;
mod wallet; mod wallet;
// TODO 6: Why are these imports required?
use crate::bitcoin::Address; use crate::bitcoin::Address;
use crate::bitcoin::FeeRate; use crate::bitcoin::Network;
use crate::bitcoin::OutPoint; use crate::bitcoin::OutPoint;
use crate::bitcoin::Psbt; use crate::bitcoin::PartiallySignedTransaction;
use crate::bitcoin::Script; use crate::bitcoin::Script;
use crate::bitcoin::Transaction; use crate::bitcoin::Transaction;
use crate::bitcoin::TxOut;
use crate::descriptor::Descriptor; use crate::descriptor::Descriptor;
use crate::error::AddressError;
use crate::error::Bip32Error;
use crate::error::Bip39Error;
use crate::error::CalculateFeeError;
use crate::error::CannotConnectError;
use crate::error::CreateTxError;
use crate::error::DescriptorError;
use crate::error::DescriptorKeyError;
use crate::error::EsploraError;
use crate::error::ExtractTxError;
use crate::error::FeeRateError;
use crate::error::PersistenceError;
use crate::error::PsbtParseError;
use crate::error::SignerError;
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::AddressInfo;
use crate::types::Balance;
use crate::types::CanonicalTx;
use crate::types::ChainPosition;
use crate::types::FullScanRequest;
use crate::types::LocalOutput;
use crate::types::ScriptAmount;
use crate::types::SyncRequest;
use crate::wallet::BumpFeeTxBuilder;
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 bdk::bitcoin::Network;
use bdk::keys::bip39::WordCount; use bdk::keys::bip39::WordCount;
use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk::wallet::tx_builder::ChangeSpendPolicy;
use bdk::wallet::AddressIndex as BdkAddressIndex;
use bdk::wallet::AddressInfo as BdkAddressInfo;
use bdk::wallet::Balance as BdkBalance;
use bdk::Error as BdkError;
use bdk::KeychainKind; use bdk::KeychainKind;
use std::sync::Arc;
uniffi::include_scaffolding!("bdk"); uniffi::include_scaffolding!("bdk");
/// A output script and an amount of satoshis.
pub struct ScriptAmount {
pub script: Arc<Script>,
pub amount: u64,
}
/// A derived address and the index it was found at.
pub struct AddressInfo {
/// Child index of this address.
pub index: u32,
/// Address.
pub address: Arc<Address>,
/// Type of keychain.
pub keychain: KeychainKind,
}
impl From<BdkAddressInfo> for AddressInfo {
fn from(address_info: BdkAddressInfo) -> Self {
AddressInfo {
index: address_info.index,
address: Arc::new(address_info.address.into()),
keychain: address_info.keychain,
}
}
}
/// The address index selection strategy to use to derived an address from the wallet's external
/// descriptor.
pub enum AddressIndex {
/// Return a new address after incrementing the current descriptor index.
New,
/// Return the address for the current descriptor index if it has not been used in a received
/// transaction. Otherwise return a new address as with AddressIndex::New.
/// Use with caution, if the wallet has not yet detected an address has been used it could
/// return an already used address. This function is primarily meant for situations where the
/// caller is untrusted; for example when deriving donation addresses on-demand for a public
/// web page.
LastUnused,
/// Return the address for a specific descriptor index. Does not change the current descriptor
/// index used by `AddressIndex::New` and `AddressIndex::LastUsed`.
/// Use with caution, if an index is given that is less than the current descriptor index
/// then the returned address may have already been used.
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),
}
}
}
// 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"),
}
}
}
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"),
}
}
}
// /// A wallet transaction
// #[derive(Debug, Clone, PartialEq, Eq, Default)]
// pub struct TransactionDetails {
// pub transaction: Option<Arc<Transaction>>,
// /// Transaction id.
// pub txid: String,
// /// Received value (sats)
// /// Sum of owned outputs of this transaction.
// pub received: u64,
// /// Sent value (sats)
// /// Sum of owned inputs of this transaction.
// pub sent: u64,
// /// Fee value (sats) if confirmed.
// /// The availability of the fee depends on the backend. It's never None with an Electrum
// /// Server backend, but it could be None with a Bitcoin RPC node without txindex that receive
// /// funds while offline.
// pub fee: Option<u64>,
// /// If the transaction is confirmed, contains height and timestamp of the block containing the
// /// transaction, unconfirmed transaction contains `None`.
// pub confirmation_time: Option<BlockTime>,
// }
//
// impl From<BdkTransactionDetails> for TransactionDetails {
// fn from(tx_details: BdkTransactionDetails) -> Self {
// let optional_tx: Option<Arc<Transaction>> =
// tx_details.transaction.map(|tx| Arc::new(tx.into()));
//
// TransactionDetails {
// transaction: optional_tx,
// fee: tx_details.fee,
// txid: tx_details.txid.to_string(),
// received: tx_details.received,
// sent: tx_details.sent,
// confirmation_time: tx_details.confirmation_time,
// }
// }
// }
//
// /// A reference to a transaction output.
// #[derive(Clone, Debug, PartialEq, Eq, Hash)]
// pub struct OutPoint {
// /// The referenced transaction's txid.
// txid: String,
// /// The index of the referenced output in its transaction's vout.
// vout: u32,
// }
//
// impl From<&OutPoint> for BdkOutPoint {
// fn from(outpoint: &OutPoint) -> Self {
// BdkOutPoint {
// txid: Txid::from_str(&outpoint.txid).unwrap(),
// vout: outpoint.vout,
// }
// }
// }
pub struct Balance {
pub inner: BdkBalance,
}
impl Balance {
/// All coinbase outputs not yet matured.
fn immature(&self) -> u64 {
self.inner.immature
}
/// Unconfirmed UTXOs generated by a wallet tx.
fn trusted_pending(&self) -> u64 {
self.inner.trusted_pending
}
/// Unconfirmed UTXOs received from an external wallet.
fn untrusted_pending(&self) -> u64 {
self.inner.untrusted_pending
}
/// Confirmed and immediately spendable balance.
fn confirmed(&self) -> u64 {
self.inner.confirmed
}
/// Get sum of trusted_pending and confirmed coins.
fn trusted_spendable(&self) -> u64 {
self.inner.trusted_spendable()
}
/// Get the whole balance visible to the wallet.
fn total(&self) -> u64 {
self.inner.total()
}
}
// impl From<BdkBalance> for Balance {
// fn from(bdk_balance: BdkBalance) -> Self {
// Balance { inner: bdk_balance }
// }
// }
// /// A transaction output, which defines new coins to be created from old ones.
// #[derive(Debug, Clone)]
// pub struct TxOut {
// /// The value of the output, in satoshis.
// value: u64,
// /// The address of the output.
// script_pubkey: Arc<Script>,
// }
//
// impl From<&BdkTxOut> for TxOut {
// fn from(tx_out: &BdkTxOut) -> Self {
// TxOut {
// value: tx_out.value,
// script_pubkey: Arc::new(Script {
// inner: tx_out.script_pubkey.clone(),
// }),
// }
// }
// }
//
// pub struct LocalUtxo {
// outpoint: OutPoint,
// txout: TxOut,
// keychain: KeychainKind,
// is_spent: bool,
// }
//
// impl From<BdkLocalUtxo> for LocalUtxo {
// fn from(local_utxo: BdkLocalUtxo) -> Self {
// LocalUtxo {
// outpoint: OutPoint {
// txid: local_utxo.outpoint.txid.to_string(),
// vout: local_utxo.outpoint.vout,
// },
// txout: TxOut {
// value: local_utxo.txout.value,
// script_pubkey: Arc::new(Script {
// inner: local_utxo.txout.script_pubkey,
// }),
// },
// keychain: local_utxo.keychain,
// is_spent: local_utxo.is_spent,
// }
// }
// }
//
// /// Trait that logs at level INFO every update received (if any).
// pub trait Progress: Send + Sync + 'static {
// /// Send a new progress update. The progress value should be in the range 0.0 - 100.0, and the message value is an
// /// optional text message that can be displayed to the user.
// fn update(&self, progress: f32, message: Option<String>);
// }
//
// struct ProgressHolder {
// progress: Box<dyn Progress>,
// }
//
// impl BdkProgress for ProgressHolder {
// fn update(&self, progress: f32, message: Option<String>) -> Result<(), BdkError> {
// self.progress.update(progress, message);
// Ok(())
// }
// }
//
// impl Debug for ProgressHolder {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// f.debug_struct("ProgressHolder").finish_non_exhaustive()
// }
// }
//
// #[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 {
// inner: tx_in.script_sig.clone(),
// }),
// sequence: tx_in.sequence.0,
// witness: tx_in.witness.to_vec(),
// }
// }
// }
// /// The method used to produce an address.
// #[derive(Debug)]
// pub enum Payload {
// /// P2PKH address.
// PubkeyHash { pubkey_hash: Vec<u8> },
// /// P2SH address.
// ScriptHash { script_hash: Vec<u8> },
// /// Segwit address.
// WitnessProgram {
// /// The witness program version.
// version: WitnessVersion,
// /// The witness program.
// program: Vec<u8>,
// },
// }
// impl From<BdkScript> for Script {
// fn from(bdk_script: BdkScript) -> Self {
// Script { inner: bdk_script }
// }
// }
//
// #[derive(Clone, Debug)]
// enum RbfValue {
// Default,
// Value(u32),
// }
//
// /// The result after calling the TxBuilder finish() function. Contains unsigned PSBT and
// /// transaction details.
// pub struct TxBuilderResult {
// pub(crate) psbt: Arc<PartiallySignedTransaction>,
// pub transaction_details: TransactionDetails,
// }
//
// uniffi::deps::static_assertions::assert_impl_all!(Wallet: Sync, Send);
//
// // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs.
// // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk`
// // crate.
// #[cfg(test)]
// mod test {
// use super::Transaction;
// use crate::Network::Regtest;
// use crate::{Address, Payload};
// use assert_matches::assert_matches;
// use bdk::bitcoin::hashes::hex::FromHex;
// use bdk::bitcoin::util::address::WitnessVersion;
//
// // Verify that bdk-ffi Transaction can be created from valid bytes and serialized back into the same bytes.
// #[test]
// fn test_transaction_serde() {
// let test_tx_bytes = Vec::from_hex("020000000001031cfbc8f54fbfa4a33a30068841371f80dbfe166211242213188428f437445c91000000006a47304402206fbcec8d2d2e740d824d3d36cc345b37d9f65d665a99f5bd5c9e8d42270a03a8022013959632492332200c2908459547bf8dbf97c65ab1a28dec377d6f1d41d3d63e012103d7279dfb90ce17fe139ba60a7c41ddf605b25e1c07a4ddcb9dfef4e7d6710f48feffffff476222484f5e35b3f0e43f65fc76e21d8be7818dd6a989c160b1e5039b7835fc00000000171600140914414d3c94af70ac7e25407b0689e0baa10c77feffffffa83d954a62568bbc99cc644c62eb7383d7c2a2563041a0aeb891a6a4055895570000000017160014795d04cc2d4f31480d9a3710993fbd80d04301dffeffffff06fef72f000000000017a91476fd7035cd26f1a32a5ab979e056713aac25796887a5000f00000000001976a914b8332d502a529571c6af4be66399cd33379071c588ac3fda0500000000001976a914fc1d692f8de10ae33295f090bea5fe49527d975c88ac522e1b00000000001976a914808406b54d1044c429ac54c0e189b0d8061667e088ac6eb68501000000001976a914dfab6085f3a8fb3e6710206a5a959313c5618f4d88acbba20000000000001976a914eb3026552d7e3f3073457d0bee5d4757de48160d88ac0002483045022100bee24b63212939d33d513e767bc79300051f7a0d433c3fcf1e0e3bf03b9eb1d70220588dc45a9ce3a939103b4459ce47500b64e23ab118dfc03c9caa7d6bfc32b9c601210354fd80328da0f9ae6eef2b3a81f74f9a6f66761fadf96f1d1d22b1fd6845876402483045022100e29c7e3a5efc10da6269e5fc20b6a1cb8beb92130cc52c67e46ef40aaa5cac5f0220644dd1b049727d991aece98a105563416e10a5ac4221abac7d16931842d5c322012103960b87412d6e169f30e12106bdf70122aabb9eb61f455518322a18b920a4dfa887d30700").unwrap();
// let new_tx_from_bytes = Transaction::new(test_tx_bytes.clone()).unwrap();
// let serialized_tx_to_bytes = new_tx_from_bytes.serialize();
// assert_eq!(test_tx_bytes, serialized_tx_to_bytes);
// }
//
// // Verify that bdk-ffi Address.payload includes expected WitnessProgram variant, version and program bytes.
// #[test]
// fn test_address_witness_program() {
// let address =
// Address::new("bcrt1qqjn9gky9mkrm3c28e5e87t5akd3twg6xezp0tv".to_string()).unwrap();
// let payload = address.payload();
// assert_matches!(payload, Payload::WitnessProgram { version, program } => {
// assert_eq!(version,WitnessVersion::V0);
// assert_eq!(program, Vec::from_hex("04a6545885dd87b8e147cd327f2e9db362b72346").unwrap());
// });
// assert_eq!(address.network(), Regtest);
// }
// }

View File

@@ -1,115 +0,0 @@
use crate::bitcoin::{Address, OutPoint, Script, Transaction, TxOut};
use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk::chain::tx_graph::CanonicalTx as BdkCanonicalTx;
use bdk::chain::{ChainPosition as BdkChainPosition, ConfirmationTimeHeightAnchor};
use bdk::wallet::AddressInfo as BdkAddressInfo;
use bdk::wallet::Balance as BdkBalance;
use bdk::KeychainKind;
use bdk::LocalOutput as BdkLocalOutput;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainPosition {
Confirmed { height: u32, timestamp: u64 },
Unconfirmed { timestamp: u64 },
}
pub struct CanonicalTx {
pub transaction: Arc<Transaction>,
pub chain_position: ChainPosition,
}
impl From<BdkCanonicalTx<'_, Arc<bdk::bitcoin::Transaction>, ConfirmationTimeHeightAnchor>>
for CanonicalTx
{
fn from(
tx: BdkCanonicalTx<'_, Arc<bdk::bitcoin::Transaction>, 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 },
};
CanonicalTx {
transaction: Arc::new(Transaction::from(tx.tx_node.tx.as_ref().clone())),
chain_position,
}
}
}
pub struct ScriptAmount {
pub script: Arc<Script>,
pub amount: u64,
}
pub struct AddressInfo {
pub index: u32,
pub address: Arc<Address>,
pub keychain: KeychainKind,
}
impl From<BdkAddressInfo> for AddressInfo {
fn from(address_info: BdkAddressInfo) -> Self {
AddressInfo {
index: address_info.index,
address: Arc::new(address_info.address.into()),
keychain: address_info.keychain,
}
}
}
pub struct Balance {
pub immature: u64,
pub trusted_pending: u64,
pub untrusted_pending: u64,
pub confirmed: u64,
pub trusted_spendable: u64,
pub total: u64,
}
impl From<BdkBalance> for Balance {
fn from(bdk_balance: BdkBalance) -> Self {
Balance {
immature: bdk_balance.immature,
trusted_pending: bdk_balance.trusted_pending,
untrusted_pending: bdk_balance.untrusted_pending,
confirmed: bdk_balance.confirmed,
trusted_spendable: bdk_balance.trusted_spendable(),
total: bdk_balance.total(),
}
}
}
pub struct LocalOutput {
pub outpoint: OutPoint,
pub txout: TxOut,
pub keychain: KeychainKind,
pub is_spent: bool,
}
impl From<BdkLocalOutput> for LocalOutput {
fn from(local_utxo: BdkLocalOutput) -> Self {
LocalOutput {
outpoint: OutPoint {
txid: local_utxo.outpoint.txid.to_string(),
vout: local_utxo.outpoint.vout,
},
txout: TxOut {
value: local_utxo.txout.value.to_sat(),
script_pubkey: Arc::new(Script(local_utxo.txout.script_pubkey)),
},
keychain: local_utxo.keychain,
is_spent: local_utxo.is_spent,
}
}
}
pub struct FullScanRequest(pub(crate) Mutex<Option<BdkFullScanRequest<KeychainKind>>>);
pub struct SyncRequest(pub(crate) Mutex<Option<BdkSyncRequest>>);

View File

@@ -1,163 +1,285 @@
use crate::bitcoin::{FeeRate, OutPoint, Psbt, Script, Transaction}; use crate::bitcoin::{OutPoint, PartiallySignedTransaction};
use crate::descriptor::Descriptor; use crate::descriptor::Descriptor;
use crate::error::{ use crate::{AddressIndex, AddressInfo, Network, ScriptAmount};
CalculateFeeError, CannotConnectError, CreateTxError, PersistenceError, SignerError, use crate::{Balance, Script};
TxidParseError, WalletCreationError, use std::collections::HashSet;
};
use crate::types::{
AddressInfo, Balance, CanonicalTx, FullScanRequest, LocalOutput, ScriptAmount, SyncRequest,
};
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::Network; use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Psbt as BdkPsbt; use bdk::wallet::Update as BdkUpdate;
use bdk::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid}; use bdk::{Error as BdkError, FeeRate};
use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk::{SignOptions, Wallet as BdkWallet};
use bdk::wallet::{ChangeSet, Update as BdkUpdate};
use bdk::Wallet as BdkWallet;
use bdk::{KeychainKind, SignOptions};
use bdk_file_store::Store;
use std::collections::HashSet; use bdk::wallet::tx_builder::ChangeSpendPolicy;
use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
const MAGIC_BYTES: &[u8] = "bdkffi".as_bytes(); #[derive(Debug)]
pub struct Wallet { pub struct Wallet {
// TODO 8: Do we really need the mutex on the wallet? Could this be an Arc?
inner_mutex: Mutex<BdkWallet>, inner_mutex: Mutex<BdkWallet>,
} }
impl Wallet { impl Wallet {
pub fn new( pub fn new_no_persist(
descriptor: Arc<Descriptor>, descriptor: Arc<Descriptor>,
change_descriptor: Option<Arc<Descriptor>>, change_descriptor: Option<Arc<Descriptor>>,
persistence_backend_path: String,
network: Network, network: Network,
) -> Result<Self, WalletCreationError> { ) -> Result<Self, BdkError> {
let descriptor = descriptor.as_string_private(); let descriptor = descriptor.as_string_private();
let change_descriptor = change_descriptor.map(|d| d.as_string_private()); let change_descriptor = change_descriptor.map(|d| d.as_string_private());
let db = Store::<ChangeSet>::open_or_create_new(MAGIC_BYTES, persistence_backend_path)?;
let wallet: BdkWallet = let wallet =
BdkWallet::new_or_load(&descriptor, change_descriptor.as_ref(), db, network)?; BdkWallet::new_no_persist(&descriptor, change_descriptor.as_ref(), network.into())?;
Ok(Wallet { Ok(Wallet {
inner_mutex: Mutex::new(wallet), inner_mutex: Mutex::new(wallet),
}) })
} }
// TODO 10: Do we need this mutex
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> { pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet") self.inner_mutex.lock().expect("wallet")
} }
pub fn reveal_next_address( pub fn get_address(&self, address_index: AddressIndex) -> AddressInfo {
&self, self.get_wallet().get_address(address_index.into()).into()
keychain_kind: KeychainKind,
) -> Result<AddressInfo, PersistenceError> {
self.get_wallet()
.reveal_next_address(keychain_kind)
.map(|address_info| address_info.into())
.map_err(|e| PersistenceError::Write {
error_message: e.to_string(),
})
}
pub fn apply_update(&self, update: Arc<Update>) -> Result<(), CannotConnectError> {
self.get_wallet()
.apply_update(update.0.clone())
.map_err(CannotConnectError::from)
}
pub fn commit(&self) -> Result<bool, PersistenceError> {
self.get_wallet()
.commit()
.map_err(|e| PersistenceError::Write {
error_message: e.to_string(),
})
} }
pub fn network(&self) -> Network { pub fn network(&self) -> Network {
self.get_wallet().network() self.get_wallet().network().into()
} }
pub fn get_balance(&self) -> Balance { pub fn get_internal_address(&self, address_index: AddressIndex) -> AddressInfo {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance(); self.get_wallet()
Balance::from(bdk_balance) .get_internal_address(address_index.into())
.into()
} }
pub fn is_mine(&self, script: &Script) -> bool { // TODO 16: Why is the Arc required here?
pub fn get_balance(&self) -> Arc<Balance> {
let bdk_balance = self.get_wallet().get_balance();
let balance = Balance { inner: bdk_balance };
Arc::new(balance)
}
pub fn apply_update(&self, update: Arc<Update>) -> Result<(), BdkError> {
self.get_wallet()
.apply_update(update.0.clone())
.map_err(|e| BdkError::Generic(e.to_string()))
}
pub fn is_mine(&self, script: Arc<Script>) -> bool {
// TODO: Both of the following lines work. Which is better?
self.get_wallet().is_mine(&script.0) self.get_wallet().is_mine(&script.0)
// self.get_wallet().is_mine(script.0.clone().as_script())
} }
/// 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( pub(crate) fn sign(
&self, &self,
psbt: Arc<Psbt>, psbt: Arc<PartiallySignedTransaction>,
// sign_options: Option<SignOptions>, // sign_options: Option<SignOptions>,
) -> Result<bool, SignerError> { ) -> Result<bool, BdkError> {
let mut psbt = psbt.0.lock().unwrap(); let mut psbt = psbt.inner.lock().unwrap();
self.get_wallet() self.get_wallet()
.sign(&mut psbt, SignOptions::default()) .sign(&mut psbt, SignOptions::default())
.map_err(SignerError::from) .map_err(|e| BdkError::Generic(e.to_string()))
} }
pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues {
let (sent, received): (u64, u64) = self.get_wallet().sent_and_received(&tx.into());
SentAndReceivedValues { sent, received }
}
pub fn transactions(&self) -> Vec<CanonicalTx> {
self.get_wallet()
.transactions()
.map(|tx| tx.into())
.collect()
}
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<u64, CalculateFeeError> {
self.get_wallet()
.calculate_fee(&tx.into())
.map_err(|e| e.into())
}
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<Arc<FeeRate>, CalculateFeeError> {
self.get_wallet()
.calculate_fee_rate(&tx.into())
.map(|bdk_fee_rate| Arc::new(FeeRate(bdk_fee_rate)))
.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 struct SentAndReceivedValues {
pub sent: u64,
pub received: u64,
} }
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,
// }
// }
// }
//
/// A transaction builder.
/// After creating the TxBuilder, you set options on it until finally calling finish to consume the builder and generate the transaction.
/// Each method on the TxBuilder returns an instance of a new TxBuilder with the option set/added.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TxBuilder { pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>, pub(crate) recipients: Vec<(BdkScriptBuf, u64)>,
@@ -165,11 +287,11 @@ pub struct TxBuilder {
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<f32>,
pub(crate) fee_absolute: Option<u64>, // pub(crate) fee_absolute: Option<u64>,
pub(crate) drain_wallet: bool, pub(crate) drain_wallet: bool,
pub(crate) drain_to: Option<BdkScriptBuf>, // pub(crate) drain_to: Option<BdkScript>,
pub(crate) rbf: Option<RbfValue>, // pub(crate) rbf: Option<RbfValue>,
// pub(crate) data: Vec<u8>, // pub(crate) data: Vec<u8>,
} }
@@ -182,15 +304,16 @@ impl TxBuilder {
change_policy: ChangeSpendPolicy::ChangeAllowed, change_policy: ChangeSpendPolicy::ChangeAllowed,
manually_selected_only: false, manually_selected_only: false,
fee_rate: None, fee_rate: None,
fee_absolute: None, // fee_absolute: None,
drain_wallet: false, drain_wallet: false,
drain_to: None, // drain_to: None,
rbf: None, // rbf: None,
// data: Vec::new(), // data: Vec::new(),
} }
} }
pub(crate) fn add_recipient(&self, script: &Script, amount: u64) -> Arc<Self> { /// Add a recipient to the internal list.
pub(crate) fn add_recipient(&self, script: Arc<Script>, amount: u64) -> Arc<Self> {
let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone(); let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone();
recipients.append(&mut vec![(script.0.clone(), amount)]); recipients.append(&mut vec![(script.0.clone(), amount)]);
@@ -211,6 +334,8 @@ impl TxBuilder {
}) })
} }
/// Add a utxo to the internal list of unspendable utxos. Its important to note that the "must-be-spent"
/// utxos added with [TxBuilder.addUtxo] have priority over this. See the Rust docs of the two linked methods for more details.
pub(crate) fn add_unspendable(&self, unspendable: OutPoint) -> Arc<Self> { pub(crate) fn add_unspendable(&self, unspendable: OutPoint) -> Arc<Self> {
let mut unspendable_hash_set = self.unspendable.clone(); let mut unspendable_hash_set = self.unspendable.clone();
unspendable_hash_set.insert(unspendable); unspendable_hash_set.insert(unspendable);
@@ -220,17 +345,15 @@ impl TxBuilder {
}) })
} }
pub(crate) fn unspendable(&self, unspendable: Vec<OutPoint>) -> Arc<Self> { /// Add an outpoint to the internal list of UTXOs that must be spent. These have priority over the "unspendable"
Arc::new(TxBuilder { /// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent.
unspendable: unspendable.into_iter().collect(),
..self.clone()
})
}
pub(crate) fn add_utxo(&self, outpoint: OutPoint) -> Arc<Self> { pub(crate) fn add_utxo(&self, outpoint: OutPoint) -> Arc<Self> {
self.add_utxos(vec![outpoint]) self.add_utxos(vec![outpoint])
} }
/// Add the list of outpoints to the internal list of UTXOs that must be spent. If an error occurs while adding
/// any of the UTXOs then none of them are added and the error is returned. These have priority over the "unspendable"
/// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent.
pub(crate) fn add_utxos(&self, mut outpoints: Vec<OutPoint>) -> Arc<Self> { pub(crate) fn add_utxos(&self, mut outpoints: Vec<OutPoint>) -> Arc<Self> {
let mut utxos = self.utxos.to_vec(); let mut utxos = self.utxos.to_vec();
utxos.append(&mut outpoints); utxos.append(&mut outpoints);
@@ -247,6 +370,7 @@ impl TxBuilder {
}) })
} }
/// Do not spend change outputs. This effectively adds all the change outputs to the "unspendable" list. See TxBuilder.unspendable.
pub(crate) fn do_not_spend_change(&self) -> Arc<Self> { pub(crate) fn do_not_spend_change(&self) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
change_policy: ChangeSpendPolicy::ChangeForbidden, change_policy: ChangeSpendPolicy::ChangeForbidden,
@@ -254,6 +378,7 @@ impl TxBuilder {
}) })
} }
/// Only spend change outputs. This effectively adds all the non-change outputs to the "unspendable" list. See TxBuilder.unspendable.
pub(crate) fn only_spend_change(&self) -> Arc<Self> { pub(crate) fn only_spend_change(&self) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
change_policy: ChangeSpendPolicy::OnlyChange, change_policy: ChangeSpendPolicy::OnlyChange,
@@ -261,6 +386,8 @@ impl TxBuilder {
}) })
} }
/// Only spend utxos added by [add_utxo]. The wallet will not add additional utxos to the transaction even if they are
/// needed to make the transaction valid.
pub(crate) fn manually_selected_only(&self) -> Arc<Self> { pub(crate) fn manually_selected_only(&self) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
manually_selected_only: true, manually_selected_only: true,
@@ -268,20 +395,32 @@ impl TxBuilder {
}) })
} }
pub(crate) fn fee_rate(&self, fee_rate: &FeeRate) -> Arc<Self> { // /// Replace the internal list of unspendable utxos with a new list. Its important to note that the "must-be-spent" utxos added with
// /// TxBuilder.addUtxo have priority over these. See the Rust docs of the two linked methods for more details.
// pub(crate) fn unspendable(&self, unspendable: Vec<OutPoint>) -> Arc<Self> {
// Arc::new(TxBuilder {
// unspendable: unspendable.into_iter().collect(),
// ..self.clone()
// })
// }
/// Set a custom fee rate.
pub(crate) fn fee_rate(&self, sat_per_vb: f32) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
fee_rate: Some(fee_rate.clone()), fee_rate: Some(sat_per_vb),
..self.clone() ..self.clone()
}) })
} }
pub(crate) fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> { // /// Set an absolute fee.
Arc::new(TxBuilder { // pub(crate) fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> {
fee_absolute: Some(fee_amount), // Arc::new(TxBuilder {
..self.clone() // fee_absolute: Some(fee_amount),
}) // ..self.clone()
} // })
// }
/// Spend all the available inputs. This respects filters like TxBuilder.unspendable and the change policy.
pub(crate) fn drain_wallet(&self) -> Arc<Self> { pub(crate) fn drain_wallet(&self) -> Arc<Self> {
Arc::new(TxBuilder { Arc::new(TxBuilder {
drain_wallet: true, drain_wallet: true,
@@ -289,28 +428,52 @@ impl TxBuilder {
}) })
} }
pub(crate) fn drain_to(&self, script: &Script) -> Arc<Self> { // /// Sets the address to drain excess coins to. Usually, when there are excess coins they are sent to a change address
Arc::new(TxBuilder { // /// generated by the wallet. This option replaces the usual change address with an arbitrary ScriptPubKey of your choosing.
drain_to: Some(script.0.clone()), // /// Just as with a change output, if the drain output is not needed (the excess coins are too small) it will not be included
..self.clone() // /// in the resulting transaction. The only difference is that it is valid to use drain_to without setting any ordinary recipients
}) // /// with add_recipient (but it is perfectly fine to add recipients as well). If you choose not to set any recipients, you should
} // /// either provide the utxos that the transaction should spend via add_utxos, or set drain_wallet to spend all of them.
// /// When bumping the fees of a transaction made with this option, you probably want to use BumpFeeTxBuilder.allow_shrinking
// /// to allow this output to be reduced to pay for the extra fees.
// pub(crate) fn drain_to(&self, script: Arc<Script>) -> Arc<Self> {
// Arc::new(TxBuilder {
// drain_to: Some(script.inner.clone()),
// ..self.clone()
// })
// }
//
// /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`.
// pub(crate) fn enable_rbf(&self) -> Arc<Self> {
// Arc::new(TxBuilder {
// rbf: Some(RbfValue::Default),
// ..self.clone()
// })
// }
//
// /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an
// /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD`
// /// an error will be thrown, since it would not be a valid nSequence to signal RBF.
// pub(crate) fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> {
// Arc::new(TxBuilder {
// rbf: Some(RbfValue::Value(nsequence)),
// ..self.clone()
// })
// }
pub(crate) fn enable_rbf(&self) -> Arc<Self> { /// Add data as an output using OP_RETURN.
Arc::new(TxBuilder { // pub(crate) fn add_data(&self, data: Vec<u8>) -> Arc<Self> {
rbf: Some(RbfValue::Default), // Arc::new(TxBuilder {
..self.clone() // data,
}) // ..self.clone()
} // })
// }
pub(crate) fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> { /// Finish building the transaction. Returns the BIP174 PSBT.
Arc::new(TxBuilder { pub(crate) fn finish(
rbf: Some(RbfValue::Value(nsequence)), &self,
..self.clone() wallet: &Wallet,
}) ) -> Result<Arc<PartiallySignedTransaction>, BdkError> {
}
pub(crate) fn finish(&self, wallet: &Arc<Wallet>) -> Result<Arc<Psbt>, CreateTxError> {
// 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();
@@ -320,106 +483,137 @@ 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();
tx_builder let utxos: &[BdkOutPoint] = &bdk_utxos;
.add_utxos(&bdk_utxos) tx_builder.add_utxos(utxos)?;
.map_err(CreateTxError::from)?;
}
if !self.unspendable.is_empty() {
let bdk_unspendable: Vec<BdkOutPoint> =
self.unspendable.iter().map(BdkOutPoint::from).collect();
tx_builder.unspendable(bdk_unspendable);
} }
// if !self.unspendable.is_empty() {
// let bdk_unspendable: Vec<BdkOutPoint> =
// self.unspendable.iter().map(BdkOutPoint::from).collect();
// tx_builder.unspendable(bdk_unspendable);
// }
if self.manually_selected_only { if self.manually_selected_only {
tx_builder.manually_selected_only(); tx_builder.manually_selected_only();
} }
if let Some(fee_rate) = &self.fee_rate { if let Some(sat_per_vb) = self.fee_rate {
tx_builder.fee_rate(fee_rate.0); tx_builder.fee_rate(FeeRate::from_sat_per_vb(sat_per_vb));
}
if let Some(fee_amount) = self.fee_absolute {
tx_builder.fee_absolute(fee_amount);
} }
// if let Some(fee_amount) = self.fee_absolute {
// tx_builder.fee_absolute(fee_amount);
// }
if self.drain_wallet { if self.drain_wallet {
tx_builder.drain_wallet(); tx_builder.drain_wallet();
} }
if let Some(script) = &self.drain_to { // if let Some(script) = &self.drain_to {
tx_builder.drain_to(script.clone()); // tx_builder.drain_to(script.clone());
} // }
if let Some(rbf) = &self.rbf { // if let Some(rbf) = &self.rbf {
match *rbf { // match *rbf {
RbfValue::Default => { // RbfValue::Default => {
tx_builder.enable_rbf(); // tx_builder.enable_rbf();
} // }
RbfValue::Value(nsequence) => { // RbfValue::Value(nsequence) => {
tx_builder.enable_rbf_with_sequence(Sequence(nsequence)); // tx_builder.enable_rbf_with_sequence(Sequence(nsequence));
} // }
} // }
} // }
// if !&self.data.is_empty() {
// tx_builder.add_data(self.data.as_slice());
// }
let psbt = tx_builder.finish().map_err(CreateTxError::from)?; // tx_builder.finish().map(|psbt| psbt.serialize_hex())
// tx_builder.finish().into()
let psbt = tx_builder.finish()?;
Ok(Arc::new(psbt.into())) Ok(Arc::new(psbt.into()))
} }
} }
#[derive(Clone)] // /// The BumpFeeTxBuilder is used to bump the fee on a transaction that has been broadcast and has its RBF flag set to true.
pub(crate) struct BumpFeeTxBuilder { // #[derive(Clone)]
pub(crate) txid: String, // pub(crate) struct BumpFeeTxBuilder {
pub(crate) fee_rate: Arc<FeeRate>, // pub(crate) txid: String,
pub(crate) rbf: Option<RbfValue>, // pub(crate) fee_rate: f32,
} // pub(crate) allow_shrinking: Option<String>,
// pub(crate) rbf: Option<RbfValue>,
impl BumpFeeTxBuilder { // }
pub(crate) fn new(txid: String, fee_rate: Arc<FeeRate>) -> Self { //
Self { // impl BumpFeeTxBuilder {
txid, // pub(crate) fn new(txid: String, fee_rate: f32) -> Self {
fee_rate, // Self {
rbf: None, // txid,
} // fee_rate,
} // allow_shrinking: None,
// rbf: None,
pub(crate) fn enable_rbf(&self) -> Arc<Self> { // }
Arc::new(Self { // }
rbf: Some(RbfValue::Default), //
..self.clone() // /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this script_pubkey
}) // /// in order to bump the transaction fee. Without specifying this the wallet will attempt to find a change output to
} // /// shrink instead. Note that the output may shrink to below the dust limit and therefore be removed. If it is preserved
// /// then it is currently not guaranteed to be in the same position as it was originally. Returns an error if script_pubkey
pub(crate) fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> { // /// cant be found among the recipients of the transaction we are bumping.
Arc::new(Self { // pub(crate) fn allow_shrinking(&self, address: String) -> Arc<Self> {
rbf: Some(RbfValue::Value(nsequence)), // Arc::new(Self {
..self.clone() // allow_shrinking: Some(address),
}) // ..self.clone()
} // })
// }
pub(crate) fn finish(&self, wallet: &Wallet) -> Result<Arc<Psbt>, CreateTxError> { //
let txid = Txid::from_str(self.txid.as_str()).map_err(|_| CreateTxError::UnknownUtxo { // /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`.
outpoint: self.txid.clone(), // pub(crate) fn enable_rbf(&self) -> Arc<Self> {
})?; // Arc::new(Self {
let mut wallet = wallet.get_wallet(); // rbf: Some(RbfValue::Default),
let mut tx_builder = wallet.build_fee_bump(txid).map_err(CreateTxError::from)?; // ..self.clone()
tx_builder.fee_rate(self.fee_rate.0); // })
if let Some(rbf) = &self.rbf { // }
match *rbf { //
RbfValue::Default => { // /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an
tx_builder.enable_rbf(); // /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD`
} // /// an error will be thrown, since it would not be a valid nSequence to signal RBF.
RbfValue::Value(nsequence) => { // pub(crate) fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> {
tx_builder.enable_rbf_with_sequence(Sequence(nsequence)); // Arc::new(Self {
} // rbf: Some(RbfValue::Value(nsequence)),
} // ..self.clone()
} // })
let psbt: BdkPsbt = tx_builder.finish()?; // }
//
Ok(Arc::new(psbt.into())) // /// Finish building the transaction. Returns the BIP174 PSBT.
} // pub(crate) fn finish(
} // &self,
// wallet: &Wallet,
#[derive(Clone, Debug)] // ) -> Result<Arc<PartiallySignedTransaction>, BdkError> {
pub enum RbfValue { // let wallet = wallet.get_wallet();
Default, // let txid = Txid::from_str(self.txid.as_str())?;
Value(u32), // let mut tx_builder = wallet.build_fee_bump(txid)?;
} // tx_builder.fee_rate(FeeRate::from_sat_per_vb(self.fee_rate));
// if let Some(allow_shrinking) = &self.allow_shrinking {
// let address = BdkAddress::from_str(allow_shrinking)
// .map_err(|e| BdkError::Generic(e.to_string()))?;
// let script = address.script_pubkey();
// tx_builder.allow_shrinking(script)?;
// }
// if let Some(rbf) = &self.rbf {
// match *rbf {
// RbfValue::Default => {
// tx_builder.enable_rbf();
// }
// RbfValue::Value(nsequence) => {
// tx_builder.enable_rbf_with_sequence(Sequence(nsequence));
// }
// }
// }
// tx_builder
// .finish()
// .map(|(psbt, _)| PartiallySignedTransaction {
// inner: Mutex::new(psbt),
// })
// .map(Arc::new)
// }
// }
// // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs.
// // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk`
// // crate.
// #[cfg(test)] // #[cfg(test)]
// mod test { // mod test {
// use crate::database::DatabaseConfig; // use crate::database::DatabaseConfig;

View File

@@ -5,7 +5,7 @@ supported bindings languages.
To skip integration tests and only run unit tests use `cargo test --lib`. To skip integration tests and only run unit tests use `cargo test --lib`.
To run all tests including integration tests use `CLASSPATH=./tests/jna/jna-5.14.0.jar cargo test`. To run all tests including integration tests use `CLASSPATH=./tests/jna/jna-5.8.0.jar cargo test`.
Before running integration tests you must install the following development tools: Before running integration tests you must install the following development tools:

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
[bindings.kotlin]
android = true
android_cleaner = true

View File

@@ -1,5 +1,5 @@
# bdk-jvm # bdk-jvm
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. 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.
## 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,6 +13,24 @@ 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 databaseConfig = DatabaseConfig.Memory
val blockchainConfig = BlockchainConfig.Electrum(
ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5u, null, 10u, true)
)
val wallet = Wallet(externalDescriptor, internalDescriptor, Network.TESTNET, databaseConfig, blockchainConfig)
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
@@ -29,17 +47,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.9.23` or later is required to build the library._ _Note that Kotlin version `1.6.10` or later is required to build the library._
1. Install JDK 17. For example, with SDKMAN!: 1. Install JDK 11. It must be version 11 (not 17), otherwise it won't build. 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 17.0.2-tem sdk install java 11.0.19-tem
``` ```
2. Install Rust (note that we are currently building using Rust 1.77.1): 2. Install Rust (note that we are currently building using Rust 1.73.0):
```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.77.1 rustup default 1.73.0
``` ```
3. Clone this repository. 3. Clone this repository.
```shell ```shell
@@ -57,7 +75,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 -P localBuild ./gradlew publishToMavenLocal --exclude-task signMavenPublication
``` ```
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:
@@ -66,7 +84,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 the `localBuild` flag: and use the `publishToMavenLocal` task without excluding the signing task:
```shell ```shell
./gradlew publishToMavenLocal ./gradlew publishToMavenLocal
``` ```

View File

@@ -1,9 +1,4 @@
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.10-SNAPSHOT libraryVersion=1.0.0-alpha.2-rc1

Binary file not shown.

View File

@@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.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/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/master/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,11 +80,13 @@ do
esac esac
done done
# This is normally unused APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # 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"'
# 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
@@ -131,29 +133,22 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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
@@ -198,10 +193,6 @@ 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
@@ -214,12 +205,6 @@ 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,8 +25,7 @@
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%
@@ -41,7 +40,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% equ 0 goto execute if "%ERRORLEVEL%" == "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.
@@ -76,15 +75,13 @@ 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% equ 0 goto mainEnd if "%ERRORLEVEL%"=="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!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 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

View File

@@ -1,23 +0,0 @@
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,15 +6,19 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
val libraryVersion: String by project val libraryVersion: String by project
plugins { plugins {
id("org.jetbrains.kotlin.jvm") id("org.jetbrains.kotlin.jvm") version "1.6.10"
id("org.gradle.java-library") id("java-library")
id("org.gradle.maven-publish") id("maven-publish")
id("org.gradle.signing") id("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
@@ -37,7 +41,7 @@ tasks.test {
testing { testing {
suites { suites {
val test by getting(JvmTestSuite::class) { val test by getting(JvmTestSuite::class) {
useKotlinTest("1.9.23") useKotlinTest("1.6.10")
} }
} }
} }
@@ -55,12 +59,10 @@ tasks.withType<Test> {
dependencies { dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom")) implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
implementation("net.java.dev.jna:jna:5.14.0") implementation("net.java.dev.jna:jna:5.8.0")
api("org.slf4j:slf4j-api:1.7.30") api("org.slf4j:slf4j-api:1.7.30")
testImplementation("junit:junit:4.13.2")
// testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
// testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1")
// testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
testImplementation("ch.qos.logback:logback-classic:1.2.3") testImplementation("ch.qos.logback:logback-classic:1.2.3")
testImplementation("ch.qos.logback:logback-core:1.2.3") testImplementation("ch.qos.logback:logback-core:1.2.3")
} }
@@ -107,10 +109,6 @@ 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

@@ -1,80 +1,29 @@
package org.bitcoindevkit package org.bitcoindevkit
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
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 {
val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db"
}
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@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.SIGNET) val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() val update = esploraClient.scan(wallet, 10uL, 1uL)
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() println("Balance: ${wallet.getBalance().total()}")
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL) assert(wallet.getBalance().total() > 0uL)
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: Psbt = TxBuilder() val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(2.0f)
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
} }
@Test
fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.SIGNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL)
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
)
val psbt: Psbt = TxBuilder()
.setRecipients(allRecipients)
.feeRate(FeeRate.fromSatPerVb(4uL))
.changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN)
.enableRbf()
.finish(wallet)
wallet.sign(psbt)
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
}
} }

View File

@@ -1,71 +1,42 @@
package org.bitcoindevkit package org.bitcoindevkit
import java.io.File
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 {
val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db"
}
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET) val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() // val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.scan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() println("Balance: ${wallet.getBalance().total()}")
println("Balance: ${wallet.getBalance().total}")
assert(wallet.getBalance().total > 0uL) assert(wallet.getBalance().total() > 0uL)
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.txid()}")
println("Sent ${sentAndReceived.sent}")
println("Received ${sentAndReceived.received}")
}
} }
@Test @Test
fun testBroadcastTransaction() { fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET) val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val fullScanRequest: FullScanRequest = wallet.startFullScan() val update = esploraClient.scan(wallet, 10uL, 1uL)
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()}")
assert(wallet.getBalance().total > 0uL) { wallet.applyUpdate(update)
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again." println("Balance: ${wallet.getBalance().total()}")
println("New address: ${wallet.getAddress(AddressIndex.New).address.asString()}")
assert(wallet.getBalance().total() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again."
} }
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: Psbt = TxBuilder() val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(2.0f)
.finish(wallet) .finish(wallet)
println(psbt.serialize()) println(psbt.serialize())
@@ -75,14 +46,8 @@ 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.txid()}")
val txFee: ULong = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}")
val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")
esploraClient.broadcast(tx) esploraClient.broadcast(tx)
} }
} }

View File

@@ -1,26 +1,10 @@
package org.bitcoindevkit package org.bitcoindevkit
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.assertFalse
class OfflineWalletTest { class OfflineWalletTest {
private val persistenceFilePath = run {
val currentDirectory = System.getProperty("user.dir")
"$currentDirectory/bdk_persistence.db"
}
@AfterTest
fun cleanup() {
val file = File(persistenceFilePath)
if (file.exists()) {
file.delete()
}
}
@Test @Test
fun testDescriptorBip86() { fun testDescriptorBip86() {
val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12) val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12)
@@ -36,18 +20,12 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet.newNoPersist(
descriptor, descriptor,
null, null,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New)
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")
assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest 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 = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e",
@@ -61,16 +39,15 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet( val wallet: Wallet = Wallet.newNoPersist(
descriptor, descriptor,
null, null,
persistenceFilePath,
Network.TESTNET Network.TESTNET
) )
assertEquals( assertEquals(
expected = 0uL, expected = 0uL,
actual = wallet.getBalance().total actual = wallet.getBalance().total()
) )
} }
} }

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

View File

@@ -1,14 +0,0 @@
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,15 +4,14 @@ 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/
rustup default 1.77.1 cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
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,16 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install -r requirements.txt pip install --user -r requirements.txt
cd ../bdk-ffi/
rustup default 1.77.1
rustup target add aarch64-apple-darwin
echo "Generating native binaries..."
cargo build --profile release-smaller --target aarch64-apple-darwin
echo "Generating bdk.py..." 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 cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
echo "Generating native binaries..."
rustup default 1.73.0
rustup target add aarch64-apple-darwin
cargo build --profile release-smaller --target aarch64-apple-darwin
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,16 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install -r requirements.txt pip install --user -r requirements.txt
cd ../bdk-ffi/
rustup default 1.77.1
rustup target add x86_64-apple-darwin
echo "Generating native binaries..."
cargo build --profile release-smaller --target x86_64-apple-darwin
echo "Generating bdk.py..." 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 cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
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
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,16 @@
set -euo pipefail set -euo pipefail
python3 --version python3 --version
pip install -r requirements.txt pip install --user -r requirements.txt
cd ../bdk-ffi/
rustup default 1.77.1
rustup target add x86_64-pc-windows-msvc
echo "Generating native binaries..."
cargo build --profile release-smaller --target x86_64-pc-windows-msvc
echo "Generating bdk.py..." 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 cd ../bdk-ffi/
cargo run --bin uniffi-bindgen generate src/bdk.udl --language python --out-dir ../bdk-python/src/bdkpython/ --no-format
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
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

@@ -13,12 +13,11 @@ pip install bdkpython
## Simple example ## Simple example
```python ```python
import bdkpython as bdk import bdkpython as bdk
```
""" """
setup( setup(
name="bdkpython", name="bdkpython",
version="1.0.0a10.dev", version="1.0.0a2.dev1",
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",

View File

@@ -1,90 +1,35 @@
import bdkpython as bdk import bdkpython as bdk
import unittest import unittest
import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" class TestLiveTxBuilder(unittest.TestCase):
TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveTxBuilderTest(unittest.TestCase):
def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"):
os.remove("./bdk_persistence.db")
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([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.SIGNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, descriptor,
None, None,
"./bdk_persistence.db", bdk.Network.TESTNET
bdk.Network.SIGNET
) )
esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL) esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
full_scan_request: bdk.FullScanRequest = wallet.start_full_scan() update = esploraClient.scan(
update = esplora_client.full_scan( wallet = wallet,
full_scan_request=full_scan_request, stop_gap = 10,
stop_gap=10, parallel_requests = 1
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0) self.assertGreater(wallet.get_balance().total(), 0)
recipient = bdk.Address( recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET 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)).finish(wallet) psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(2.0).finish(wallet)
# print(psbt.serialize())
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.SIGNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
bdk.Network.SIGNET
)
wallet: bdk.Wallet = bdk.Wallet(
descriptor,
change_descriptor,
"./bdk_persistence.db",
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)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
recipient1 = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
)
recipient2 = bdk.Address(
address="tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6",
network=bdk.Network.SIGNET
)
all_recipients = list(
bdk.ScriptAmount(recipient1.script_pubkey, 4200),
bdk.ScriptAmount(recipient2.script_pubkey, 4200)
)
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)
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

@@ -1,90 +1,62 @@
import bdkpython as bdk import bdkpython as bdk
import unittest import unittest
import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" class TestLiveWallet(unittest.TestCase):
TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveWalletTest(unittest.TestCase):
def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"):
os.remove("./bdk_persistence.db")
def test_synced_balance(self): def test_synced_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.SIGNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, descriptor,
None, None,
"./bdk_persistence.db", bdk.Network.TESTNET
bdk.Network.SIGNET
) )
esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL) esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
full_scan_request: bdk.FullScanRequest = wallet.start_full_scan() update = esploraClient.scan(
update = esplora_client.full_scan( wallet = wallet,
full_scan_request=full_scan_request, stop_gap = 10,
stop_gap=10, parallel_requests = 1
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
print(f"Transactions count: {len(wallet.transactions())}")
transactions = wallet.transactions()[:3]
for tx in transactions:
sent_and_received = wallet.sent_and_received(tx.transaction)
print(f"Transaction: {tx.transaction.txid()}")
print(f"Sent {sent_and_received.sent}")
print(f"Received {sent_and_received.received}")
self.assertGreater(wallet.get_balance().total(), 0)
def test_broadcast_transaction(self): def test_broadcast_transaction(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, descriptor,
None, None,
"./bdk_persistence.db", bdk.Network.TESTNET
bdk.Network.SIGNET
) )
esplora_client: bdk.EsploraClient = bdk.EsploraClient(url = SIGNET_ESPLORA_URL) esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
full_scan_request: bdk.FullScanRequest = wallet.start_full_scan() update = esploraClient.scan(
update = esplora_client.full_scan( wallet = wallet,
full_scan_request=full_scan_request, stop_gap = 10,
stop_gap=10, parallel_requests = 1
parallel_requests=1
) )
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0) self.assertGreater(wallet.get_balance().total(), 0)
recipient = bdk.Address( recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET network = bdk.Network.TESTNET
) )
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet) psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(2.0).finish(wallet)
# print(psbt.serialize()) # print(psbt.serialize())
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()}")
fee = wallet.calculate_fee(tx)
print(f"Transaction Fee: {fee}")
fee_rate = wallet.calculate_fee_rate(tx)
print(f"Transaction Fee Rate: {fee_rate.to_sat_per_vb_ceil()} sat/vB")
esplora_client.broadcast(tx) esploraClient.broadcast(tx)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,7 +1,7 @@
import bdkpython as bdk import bdkpython as bdk
import unittest import unittest
class OfflineDescriptorTest(unittest.TestCase): class TestSimpleWallet(unittest.TestCase):
def test_descriptor_bip86(self): def test_descriptor_bip86(self):
mnemonic: bdk.Mnemonic = bdk.Mnemonic.from_string("space echo position wrist orient erupt relief museum myself grain wisdom tumble") mnemonic: bdk.Mnemonic = bdk.Mnemonic.from_string("space echo position wrist orient erupt relief museum myself grain wisdom tumble")

View File

@@ -1,30 +1,19 @@
import bdkpython as bdk import bdkpython as bdk
import unittest import unittest
import os
class OfflineWalletTest(unittest.TestCase): class TestSimpleWallet(unittest.TestCase):
def tearDown(self) -> None:
if os.path.exists("./bdk_persistence.db"):
os.remove("./bdk_persistence.db")
def test_new_address(self): def test_new_address(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: Wallet = bdk.Wallet( wallet: Wallet = bdk.Wallet.new_no_persist(
descriptor, descriptor,
None, None,
"./bdk_persistence.db",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
address_info: bdk.AddressInfo = wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL) address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW())
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.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.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string()) self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string())
@@ -33,14 +22,13 @@ class OfflineWalletTest(unittest.TestCase):
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, descriptor,
None, None,
"./bdk_persistence.db",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
self.assertEqual(wallet.get_balance().total, 0) self.assertEqual(wallet.get_balance().total(), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -29,7 +29,8 @@ let package = Package(
.binaryTarget(name: "bdkFFI", path: "./bdkFFI.xcframework"), .binaryTarget(name: "bdkFFI", path: "./bdkFFI.xcframework"),
.target( .target(
name: "BitcoinDevKit", name: "BitcoinDevKit",
dependencies: ["bdkFFI"] dependencies: ["bdkFFI"],
swiftSettings: [.unsafeFlags(["-suppress-warnings"])]
), ),
.testTarget( .testTarget(
name: "BitcoinDevKitTests", name: "BitcoinDevKitTests",

View File

@@ -1,107 +1,34 @@
import XCTest import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTxBuilderTests: XCTestCase { final class LiveTxBuilderTests: XCTestCase {
var dbFilePath: URL!
override func setUpWithError() throws {
super.setUp()
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).db"
dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName)
if fileManager.fileExists(atPath: dbFilePath.path) {
try fileManager.removeItem(at: dbFilePath)
}
}
override func tearDownWithError() throws {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: dbFilePath.path) {
try fileManager.removeItem(at: dbFilePath)
}
}
func testTxBuilder() throws { func testTxBuilder() 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.signet network: Network.testnet
) )
let wallet = try Wallet( let wallet = try Wallet.newNoPersist(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path, network: .testnet
network: .signet
) )
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let fullScanRequest: FullScanRequest = wallet.startFullScan() let update = try esploraClient.scan(
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)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0), "Wallet must have positive balance, please add funds")
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: Psbt = try TxBuilder() let psbt: PartiallySignedTransaction = try TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) .addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2)) .feeRate(satPerVbyte: 2.0)
.finish(wallet: wallet) .finish(wallet: wallet)
print(psbt.serialize()) print(psbt.serialize())
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
} }
func testComplexTxBuilder() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
let changeDescriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
let wallet = try Wallet(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
persistenceBackendPath: dbFilePath.path,
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)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet)
let allRecipients: [ScriptAmount] = [
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200),
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200)
]
let psbt: Psbt = try TxBuilder()
.setRecipients(recipients: allRecipients)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 4))
.changePolicy(changePolicy: ChangeSpendPolicy.changeForbidden)
.enableRbf()
.finish(wallet: wallet)
try! wallet.sign(psbt: psbt)
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
}
} }

View File

@@ -1,94 +1,55 @@
import XCTest import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveWalletTests: XCTestCase { final class LiveWalletTests: XCTestCase {
var dbFilePath: URL!
override func setUpWithError() throws {
super.setUp()
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).db"
dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName)
if fileManager.fileExists(atPath: dbFilePath.path) {
try fileManager.removeItem(at: dbFilePath)
}
}
override func tearDownWithError() throws {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: dbFilePath.path) {
try fileManager.removeItem(at: dbFilePath)
}
}
func testSyncedBalance() throws { func testSyncedBalance() 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.signet network: Network.testnet
) )
let wallet = try Wallet( let wallet = try Wallet.newNoPersist(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path, network: .testnet
network: .signet
) )
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let fullScanRequest: FullScanRequest = wallet.startFullScan() let update = try esploraClient.scan(
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)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0)) XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0))
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.txid())")
print("Sent \(sentAndReceived.sent)")
print("Received \(sentAndReceived.received)")
}
} }
func testBroadcastTransaction() throws { func testBroadcastTransaction() 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.signet network: Network.testnet
) )
let wallet = try Wallet( let wallet = try Wallet.newNoPersist(
descriptor: descriptor, descriptor: descriptor,
changeDescriptor: nil, changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path, network: .testnet
network: .signet
) )
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let fullScanRequest: FullScanRequest = wallet.startFullScan() let update = try esploraClient.scan(
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)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0), "Wallet must have positive balance, please add funds")
print("Balance: \(wallet.getBalance().total)") print("Balance: \(wallet.getBalance().total())")
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: Psbt = try let psbt: PartiallySignedTransaction = try
TxBuilder() TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) .addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2)) .feeRate(satPerVbyte: 2.0)
.finish(wallet: wallet) .finish(wallet: wallet)
print(psbt.serialize()) print(psbt.serialize())
@@ -97,13 +58,8 @@ 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 = try! psbt.extractTx() let tx: Transaction = psbt.extractTx()
print(tx.txid()) print(tx.txid())
let fee: UInt64 = try wallet.calculateFee(tx: tx)
print("Transaction Fee: \(fee)")
let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx)
print("Transaction Fee Rate: \(feeRate.toSatPerVbCeil()) sat/vB")
try esploraClient.broadcast(transaction: tx) try esploraClient.broadcast(transaction: tx)
} }
} }

View File

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

View File

@@ -16,8 +16,6 @@
</array> </array>
<key>SupportedPlatform</key> <key>SupportedPlatform</key>
<string>macos</string> <string>macos</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict> </dict>
<dict> <dict>
<key>LibraryIdentifier</key> <key>LibraryIdentifier</key>
@@ -33,8 +31,6 @@
<string>ios</string> <string>ios</string>
<key>SupportedPlatformVariant</key> <key>SupportedPlatformVariant</key>
<string>simulator</string> <string>simulator</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict> </dict>
<dict> <dict>
<key>LibraryIdentifier</key> <key>LibraryIdentifier</key>
@@ -47,8 +43,6 @@
</array> </array>
<key>SupportedPlatform</key> <key>SupportedPlatform</key>
<string>ios</string> <string>ios</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict> </dict>
</array> </array>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>

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,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,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

@@ -3,15 +3,17 @@
# The results of this script can be used for locally testing your SPM package adding a local package # 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. # to your application pointing at the bdk-swift directory.
rustup default 1.77.1 # Run the script from the repo root directory, ie: ./bdk-swift/build-local-swift.sh
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 rustup install 1.73.0
rustup component add rust-src
rustup target add aarch64-apple-ios x86_64-apple-ios
rustup target add aarch64-apple-ios-sim
rustup target add aarch64-apple-darwin x86_64-apple-darwin
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 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 aarch64-apple-darwin
@@ -19,14 +21,13 @@ cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-io
cargo build --package bdk-ffi --profile release-smaller --target aarch64-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 cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios-sim
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
mkdir -p target/lipo-ios-sim/release-smaller 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 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 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 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 popd
pushd bdk-swift
mv Sources/BitcoinDevKit/bdk.swift Sources/BitcoinDevKit/BitcoinDevKit.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/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/ios-arm64_x86_64-simulator/bdkFFI.framework/Headers
@@ -36,3 +37,5 @@ cp ../bdk-ffi/target/lipo-ios-sim/release-smaller/libbdkffi.a bdkFFI.xcframework
cp ../bdk-ffi/target/lipo-macos/release-smaller/libbdkffi.a bdkFFI.xcframework/macos-arm64_x86_64/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.h
rm Sources/BitcoinDevKit/bdkFFI.modulemap rm Sources/BitcoinDevKit/bdkFFI.modulemap
#rm bdkFFI.xcframework.zip || true
#zip -9 -r bdkFFI.xcframework.zip bdkFFI.xcframework

View File

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

View File

@@ -1,23 +0,0 @@
# Naming convention
Producing language bindings potentially requires renaming a number of types and methods, and this document outlines the approach we have decided to take when thinking through this problem for bdk-ffi libraries.
## Context and Problem Statement
The tool we use to produce language bindings for bdk-ffi libraries is [uniffi]. While the library is powerful, it also comes with some caveats. Some of those include the inability to expose to foreign bindings Rust-specific types like tuples, and the inability to expose generics. This means that at least _some_ wrapping and transforming of certain things are required between the pure Rust code coming from the bdk library and the final language bindings in Swift, Kotlin, and Python.
With wrapping comes (a) the requirement for naming potentially new types, and (b) the ability to "wrap" behaviour that could be useful for end users. This document addresses point (a).
## Decision Drivers
Our main goals are:
1. Keep the multiple language bindings libraries maintainable.
2. Help users of bdk help each other and working with a similarly shaped API across languages.
## Decision Outcome
We decided to try and keep the names of all types the same between the Rust libraries and the bindings, and in cases where new types had to be created, to keep them in the style and spirit of the bdk and rust-bitcoin libraries.
There is so far one exception to this rule, where we renamed the `ScriptBuf` type from rust-bitcoin to `Script`. This was done because the concept of owned vs. borrowed types is strictly a Rust concept, and is not passed onto the languages bindings in any way, and therefore keeping the script type as `Script` was our preferred option in this case.
[uniffi]: https://github.com/mozilla/uniffi-rs/

View File

@@ -1,26 +0,0 @@
# Wrapping BDK APIs
Producing language bindings potentially requires wrapping a number of APIs, and this document outlines the approach we have decided to take when thinking through this problem for bdk-ffi libraries.
## Context and Problem Statement
The tool we use to produce language bindings for bdk-ffi libraries is [uniffi]. While the library is powerful, it also comes with some caveats. Some of those include the inability to expose to foreign bindings Rust-specific types like tuples, and the inability to expose generics. This means that at least _some_ wrapping and transforming of certain things are required between the pure Rust code coming from the bdk library and the final language bindings in Swift, Kotlin, and Python.
With wrapping comes (a) the requirement for naming potentially new types, and (b) the ability to "wrap" behaviour that could be useful for end users. This document addresses point (b).
## Decision Drivers
Our main goals are:
1. Keep the multiple language bindings libraries maintainable.
2. Help users of bdk help each other and working with a similarly shaped API across languages.
## Decision Outcome
There are three potential reasons for wrapping Rust BDK APIs:
1. The Rust types are not available in the target language (e.g., a function returns a tuple, which can't be returned in Swift/Kotlin)
2. Some complex functionality is available in the Rust bitcoin/miniscript/bdk ecosystem, but exposing all underlying types required for this functionality is out of scope at the time a particular feature is required
3. Some extra feature/utility might be interesting for our end-users
Our approach with the bdk-ffi libraries is to only provide wrapping for cases (1) and (2) mentioned above. If extra functionality to the BDK API would be useful, we open issues upstream and merge those in Rust first, and then expose it in our bindings. This approach favors (a) keeping the bindings libraries as thin as possible, minimizing the potential for integrating bugs at the bindings layer, and (b) keeping the API as close as we can to Rust BDK, promoting collaboration between users of BDK across languages, including with teams that use BDK in bindings (mobile) and server-side (Rust).
[uniffi]: https://github.com/mozilla/uniffi-rs/

View File

@@ -1,12 +0,0 @@
# Architectural Decision Records
This directory contains a series of Architectural Decision Records or "ADRs" for the bdk-ffi project. We're going to use it as a kind of collective memory of the decisions we've made and the path we've taken to get the project to its current point.
A good example of simple and well executed ADRs can be found on the [uniffi](https://github.com/mozilla/uniffi-rs/) project repository. See their [readme](https://github.com/mozilla/uniffi-rs/tree/main/docs/adr) and [template](https://github.com/mozilla/uniffi-rs/blob/main/docs/adr/template.md) for more information.
Some more readings on ADRs:
- https://www.ozimmer.ch/practices/2023/04/03/ADRCreation.html
- https://github.com/joelparkerhenderson/architecture-decision-record
- https://adr.github.io/
- https://betterprogramming.pub/the-ultimate-guide-to-architectural-decision-records-6d74fd3850ee
- https://www.redhat.com/architect/architecture-decision-records