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:
matrix:
rust:
- version: 1.77.1
- version: 1.73.0
clippy: true
steps:
- name: "Checkout"
@@ -56,7 +56,7 @@ jobs:
run: cargo clippy --all-targets --features "uniffi/bindgen-tests" -- -D warnings
- 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:
name: "Rust fmt"

View File

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

View File

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

View File

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

View File

@@ -24,19 +24,16 @@ jobs:
- cp38-cp38
- cp39-cp39
- cp310-cp310
- cp311-cp311
- cp312-cp312
steps:
- name: "Checkout"
uses: actions/checkout@v3
with:
submodules: true
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
# TODO 2: Other CI workflows use explicit Rust compiler versions, I think we should do the same here
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.77.1
toolchain: stable
- name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh
@@ -62,8 +59,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3
@@ -101,8 +96,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3
@@ -139,8 +132,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3

View File

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

View File

@@ -33,19 +33,15 @@ jobs:
- cp38-cp38
- cp39-cp39
- cp310-cp310
- cp311-cp311
- cp312-cp312
steps:
- name: "Checkout"
uses: actions/checkout@v3
with:
submodules: true
- name: "Install Rust 1.77.1"
uses: actions-rs/toolchain@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.77.1
toolchain: stable
- name: "Generate bdk.py and binaries"
run: bash ./scripts/generate-linux.sh
@@ -78,8 +74,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3
@@ -123,8 +117,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3
@@ -166,8 +158,6 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- name: "Checkout"
uses: actions/checkout@v3

View File

@@ -19,8 +19,7 @@ jobs:
uses: actions/checkout@v3
- name: "Build Swift package"
working-directory: bdk-swift
run: bash ./build-local-swift.sh
run: bash ./bdk-swift/build-local-swift.sh
- name: "Run Swift tests"
working-directory: bdk-swift

View File

@@ -1,30 +1,8 @@
# 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.
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
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).
## [0.30.0]
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
[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.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

View File

@@ -8,7 +8,7 @@
</p>
## 🚨 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
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] | |
| 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)
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
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
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
To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block:
```kotlin
@@ -31,41 +49,36 @@ dependencies {
* [Padawan Wallet](https://github.com/thunderbiscuit/padawan-wallet)
### 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.
```shell
git clone https://github.com/bitcoindevkit/bdk-ffi
```
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
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
```sh
rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi
```
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
# macOS
export ANDROID_SDK_ROOT=~/Library/Android/sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653
# linux
export ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
export ANDROID_SDK_ROOT=~/Android/Sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/25.2.9519653
```
7. Build kotlin bindings
```sh
# build Android library
cd bdk-android
./gradlew buildAndroidLib
```
1. Start android emulator and run tests
8. Start android emulator (must be x86_64) and run tests
```sh
./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
```shell
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:
@@ -82,7 +95,7 @@ signing.gnupg.keyName=<YOUR_GNUPG_ID>
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
./gradlew publishToMavenLocal
```

View File

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

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

41
bdk-android/gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,11 +80,13 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
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.
MAX_FD=maximum
@@ -131,29 +133,22 @@ location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which java >/dev/null 2>&1 || 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
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
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 ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | 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" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then
done
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:
# * 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.
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -214,12 +205,6 @@ set -- \
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.
#
# 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
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
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 {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.gradle.maven-publish")
id("org.gradle.signing")
id("org.jetbrains.kotlin.android") version "1.6.10"
id("maven-publish")
id("signing")
// Custom plugin to generate the native libs and bindings file
id("org.bitcoindevkit.plugins.generate-android-bindings")
}
repositories {
mavenCentral()
google()
}
android {
namespace = "org.bitcoindevkit"
compileSdk = 34
compileSdk = 31
defaultConfig {
minSdk = 21
targetSdk = 34
targetSdk = 31
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
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 {
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("androidx.appcompat:appcompat:1.4.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 {
if (project.hasProperty("localBuild")) {
isRequired = false
}
val signingKeyId: String? by project
val signingKey: String? by project
val signingPassword: String? by project
@@ -129,7 +106,8 @@ signing {
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> {
dependsOn("buildAndroidLib")
}

View File

@@ -1,81 +1,30 @@
package org.bitcoindevkit
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import java.io.File
import kotlin.test.AfterTest
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)
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
fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
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 psbt: Psbt = TxBuilder()
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2uL))
.feeRate(2.0f)
.finish(wallet)
println(psbt.serialize())
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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.RunWith
import java.io.File
import kotlin.test.AfterTest
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)
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
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api")
// val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total()}")
val balance: Balance = wallet.getBalance()
println("Balance: $balance")
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}")
}
assert(wallet.getBalance().total() > 0uL)
}
@Test
fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet = Wallet(descriptor, null, 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}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address}")
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
assert(wallet.getBalance().total > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
wallet.applyUpdate(update)
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)
.feeRate(FeeRate.fromSatPerVb(4uL))
.feeRate(4.0f)
.finish(wallet)
println(psbt.serialize())
@@ -79,14 +51,8 @@ class LiveWalletTest {
assertTrue(walletDidSign)
val tx: Transaction = psbt.extractTx()
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)
}
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package org.bitcoindevkit.plugins
val operatingSystem: OS = when {
System.getProperty("os.name").contains("mac", ignoreCase = true) -> OS.MAC
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")
}
// 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
val buildAndroidAarch64Binary by tasks.register<Exec>("buildAndroidAarch64Binary") {
@@ -31,6 +26,13 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
executable("cargo")
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(
// add build toolchain to PATH
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")
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(
// add build toolchain to PATH
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")
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(
// add build toolchain to PATH
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") {
dependsOn(buildAndroidAarch64Binary)
dependsOn(buildAndroidArmv7Binary)
dependsOn(buildAndroidX86_64Binary)
into("${project.projectDir}/../lib/src/main/jniLibs/")
@@ -123,14 +137,14 @@ internal class UniFfiAndroidPlugin : Plugin<Project> {
val generateAndroidBindings by tasks.register<Exec>("generateAndroidBindings") {
dependsOn(moveNativeAndroidLibs)
val libraryPath = "${project.projectDir}/../../bdk-ffi/target/aarch64-linux-android/release-smaller/libbdkffi.so"
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 libraryPath = "${project.projectDir}/../../bdk-ffi/target/aarch64-linux-android/release-smaller/libbdkffi.so"
// 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")
// The code above worked for uniffi 0.24.3 using the --library flag
// The code below works for uniffi 0.23.0
// 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")
workingDir("${project.projectDir}/../../bdk-ffi")
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")
args(cargoArgs)

View File

@@ -2,17 +2,3 @@ rootProject.name = "bdk-android"
include(":lib")
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]
name = "bdk-ffi"
version = "1.0.0-alpha.10"
version = "1.0.0-alpha.2"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
edition = "2018"
@@ -18,19 +18,24 @@ path = "uniffi-bindgen.rs"
default = ["uniffi/cli"]
[dependencies]
bdk = { version = "1.0.0-alpha.10", 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" }
bdk = { version = "1.0.0-alpha.2", features = ["all-keys", "keys-bip39"] }
uniffi = { version = "=0.26.1" }
bitcoin-internals = { version = "0.2.0", features = ["alloc"] }
thiserror = "1.0.58"
# TODO 22: The bdk_esplora crate uses esplora_client which uses reqwest for async. By default it uses the system
# openssl library, which is creating problems for cross-compilation. I'd rather use rustls, but it's hidden
# 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]
uniffi = { version = "=0.26.1", features = ["build"] }
uniffi = { version = "=0.25.1", features = ["build"] }
[dev-dependencies]
uniffi = { version = "=0.26.1", features = ["bindgen-tests"] }
uniffi = { version = "=0.25.1", features = ["bindgen-tests"] }
assert_matches = "1.5.0"
[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 {};
// ------------------------------------------------------------------------
// bdk crate - error 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
// bdk crate - root module
// ------------------------------------------------------------------------
enum KeychainKind {
@@ -196,105 +9,92 @@ enum KeychainKind {
"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 {
u32 index;
Address address;
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]
interface ChainPosition {
Confirmed(u32 height, u64 timestamp);
Unconfirmed(u64 timestamp);
};
dictionary CanonicalTx {
Transaction transaction;
ChainPosition chain_position;
};
interface FullScanRequest {};
interface SyncRequest {};
// ------------------------------------------------------------------------
// bdk crate - wallet module
// ------------------------------------------------------------------------
enum ChangeSpendPolicy {
"ChangeAllowed",
"OnlyChange",
"ChangeForbidden"
interface AddressIndex {
New();
LastUnused();
Peek(u32 index);
};
interface Wallet {
[Throws=WalletCreationError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network);
[Name=new_no_persist, Throws=BdkError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, Network network);
[Throws=PersistenceError]
AddressInfo reveal_next_address(KeychainKind keychain);
AddressInfo get_address(AddressIndex address_index);
AddressInfo get_internal_address(AddressIndex address_index);
Network network();
Balance get_balance();
[Throws=CannotConnectError]
boolean is_mine(Script script);
[Throws=BdkError]
void apply_update(Update update);
[Throws=PersistenceError]
boolean commit();
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();
[Throws=BdkError]
boolean sign(PartiallySignedTransaction psbt);
};
interface Update {};
@@ -302,14 +102,12 @@ interface Update {};
interface TxBuilder {
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 unspendable(sequence<OutPoint> unspendable);
TxBuilder add_utxo(OutPoint outpoint);
TxBuilder change_policy(ChangeSpendPolicy change_policy);
@@ -320,31 +118,12 @@ interface TxBuilder {
TxBuilder manually_selected_only();
TxBuilder fee_rate([ByRef] FeeRate fee_rate);
TxBuilder fee_absolute(u64 fee);
TxBuilder fee_rate(float sat_per_vbyte);
TxBuilder drain_wallet();
TxBuilder drain_to([ByRef] Script script);
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);
[Throws=BdkError]
PartiallySignedTransaction finish([ByRef] Wallet wallet);
};
// ------------------------------------------------------------------------
@@ -354,31 +133,31 @@ interface BumpFeeTxBuilder {
interface Mnemonic {
constructor(WordCount word_count);
[Name=from_string, Throws=Bip39Error]
[Name=from_string, Throws=BdkError]
constructor(string mnemonic);
[Name=from_entropy, Throws=Bip39Error]
[Name=from_entropy, Throws=BdkError]
constructor(sequence<u8> entropy);
string as_string();
};
interface DerivationPath {
[Throws=Bip32Error]
[Throws=BdkError]
constructor(string path);
};
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);
[Throws=DescriptorKeyError]
DescriptorSecretKey derive([ByRef] DerivationPath path);
[Throws=BdkError]
DescriptorSecretKey derive(DerivationPath path);
[Throws=DescriptorKeyError]
DescriptorSecretKey extend([ByRef] DerivationPath path);
[Throws=BdkError]
DescriptorSecretKey extend(DerivationPath path);
DescriptorPublicKey as_public();
@@ -388,45 +167,45 @@ interface DescriptorSecretKey {
};
interface DescriptorPublicKey {
[Name=from_string, Throws=DescriptorKeyError]
[Name=from_string, Throws=BdkError]
constructor(string public_key);
[Throws=DescriptorKeyError]
DescriptorPublicKey derive([ByRef] DerivationPath path);
[Throws=BdkError]
DescriptorPublicKey derive(DerivationPath path);
[Throws=DescriptorKeyError]
DescriptorPublicKey extend([ByRef] DerivationPath path);
[Throws=BdkError]
DescriptorPublicKey extend(DerivationPath path);
string as_string();
};
interface Descriptor {
[Throws=DescriptorError]
[Throws=BdkError]
constructor(string descriptor, Network network);
[Name=new_bip44]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[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]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[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]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[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]
constructor([ByRef] DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
constructor(DescriptorSecretKey secret_key, KeychainKind keychain, Network network);
[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();
@@ -440,14 +219,11 @@ interface Descriptor {
interface EsploraClient {
constructor(string url);
[Throws=EsploraError]
Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 parallel_requests);
[Throws=BdkError]
Update scan(Wallet wallet, u64 stop_gap, u64 parallel_requests);
[Throws=EsploraError]
Update sync(SyncRequest sync_request, u64 parallel_requests);
[Throws=EsploraError]
void broadcast([ByRef] Transaction transaction);
[Throws=BdkError]
void broadcast(Transaction transaction);
};
// ------------------------------------------------------------------------
@@ -459,11 +235,6 @@ dictionary ScriptAmount {
u64 amount;
};
dictionary SentAndReceivedValues {
u64 sent;
u64 received;
};
// ------------------------------------------------------------------------
// bdk crate - bitcoin re-exports
// ------------------------------------------------------------------------
@@ -474,7 +245,6 @@ interface Script {
sequence<u8> to_bytes();
};
[NonExhaustive]
enum Network {
"Bitcoin",
"Testnet",
@@ -491,7 +261,7 @@ enum WordCount {
};
interface Address {
[Throws=AddressError]
[Throws=BdkError]
constructor(string address, Network network);
Network network();
@@ -501,40 +271,33 @@ interface Address {
string to_qr_uri();
string as_string();
boolean is_valid_for_network(Network network);
};
interface Transaction {
[Throws=TransactionError]
[Throws=BdkError]
constructor(sequence<u8> transaction_bytes);
string txid();
u64 total_size();
u64 size();
u64 vsize();
boolean is_coinbase();
boolean is_coin_base();
boolean is_explicitly_rbf();
boolean is_lock_time_enabled();
i32 version();
sequence<u8> serialize();
u64 weight();
};
interface Psbt {
[Throws=PsbtParseError]
interface PartiallySignedTransaction {
[Throws=BdkError]
constructor(string psbt_base64);
string serialize();
[Throws=ExtractTxError]
Transaction extract_tx();
};
@@ -542,17 +305,3 @@ dictionary OutPoint {
string txid;
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::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::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::FeeRate as BdkFeeRate;
use bdk::bitcoin::Network;
use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::bitcoin::Txid;
use bdk::Error as BdkError;
use std::io::Cursor;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
/// A Bitcoin script.
#[derive(Clone, Debug, PartialEq, Eq)]
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)]
pub struct Address(BdkAddress<NetworkChecked>);
pub struct Address {
inner: BdkAddress<NetworkChecked>,
}
impl Address {
pub fn new(address: String, network: Network) -> Result<Self, AddressError> {
let parsed_address = address.parse::<bdk::bitcoin::Address<NetworkUnchecked>>()?;
let network_checked_address = parsed_address.require_network(network)?;
pub fn new(address: String, network: Network) -> Result<Self, BdkError> {
let parsed_address = address
.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 {
*self.0.network()
self.inner.network.into()
}
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 {
self.0.to_qr_uri()
self.inner.to_qr_uri()
}
pub fn as_string(&self) -> String {
self.0.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
}
self.inner.to_string()
}
}
impl From<Address> for BdkAddress {
fn from(address: Address) -> Self {
address.0
address.inner
}
}
impl From<BdkAddress> for Address {
fn from(address: BdkAddress) -> Self {
Address(address)
Address { inner: address }
}
}
/// A Bitcoin transaction.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transaction(BdkTransaction);
pub struct Transaction {
inner: BdkTransaction,
}
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 tx: BdkTransaction = BdkTransaction::consensus_decode(&mut decoder)?;
Ok(Transaction(tx))
let tx: BdkTransaction = BdkTransaction::consensus_decode(&mut decoder)
.map_err(|e| BdkError::Generic(e.to_string()))?;
Ok(Transaction { inner: tx })
}
pub fn txid(&self) -> String {
self.0.txid().to_string()
self.inner.txid().to_string()
}
pub fn weight(&self) -> u64 {
self.0.weight().to_wu()
}
// fn weight(&self) -> u64 {
// self.inner.weight() as u64
// }
pub fn total_size(&self) -> u64 {
self.0.total_size() as u64
pub fn size(&self) -> u64 {
self.inner.size() as u64
}
pub fn vsize(&self) -> u64 {
self.0.vsize() as u64
self.inner.vsize() as u64
}
pub fn is_coinbase(&self) -> bool {
self.0.is_coinbase()
// fn serialize(&self) -> Vec<u8> {
// self.inner.serialize()
// }
pub fn is_coin_base(&self) -> bool {
self.inner.is_coin_base()
}
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 {
self.0.is_lock_time_enabled()
self.inner.is_lock_time_enabled()
}
pub fn version(&self) -> i32 {
self.0.version.0
self.inner.version
}
pub fn serialize(&self) -> Vec<u8> {
serialize(&self.0)
}
// fn lock_time(&self) -> u32 {
// 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 {
fn from(tx: BdkTransaction) -> Self {
Transaction(tx)
Transaction { inner: tx }
}
}
impl From<&BdkTransaction> for Transaction {
fn from(tx: &BdkTransaction) -> Self {
Transaction(tx.clone())
impl From<Transaction> for BdkTransaction {
fn from(tx: Transaction) -> Self {
tx.inner
}
}
impl From<&Transaction> for BdkTransaction {
fn from(tx: &Transaction) -> Self {
tx.0.clone()
}
pub struct PartiallySignedTransaction {
pub(crate) inner: Mutex<BdkPartiallySignedTransaction>,
}
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 {
pub(crate) fn new(psbt_base64: String) -> Result<Self, PsbtParseError> {
let psbt: BdkPsbt = BdkPsbt::from_str(&psbt_base64)?;
Ok(Psbt(Mutex::new(psbt)))
Ok(PartiallySignedTransaction {
inner: Mutex::new(psbt),
})
}
pub(crate) fn serialize(&self) -> String {
let psbt = self.0.lock().unwrap().clone();
let psbt = self.inner.lock().unwrap().clone();
psbt.to_string()
}
pub(crate) fn extract_tx(&self) -> Result<Arc<Transaction>, ExtractTxError> {
let tx: BdkTransaction = self.0.lock().unwrap().clone().extract_tx()?;
let transaction: Transaction = tx.into();
Ok(Arc::new(transaction))
}
}
impl From<BdkPsbt> for Psbt {
fn from(psbt: BdkPsbt) -> Self {
Psbt(Mutex::new(psbt))
// pub(crate) fn txid(&self) -> String {
// let tx = self.inner.lock().unwrap().clone().extract_tx();
// let txid = tx.txid();
// txid.to_hex()
// }
/// Return the transaction.
pub(crate) fn extract_tx(&self) -> Arc<Transaction> {
let tx = self.inner.lock().unwrap().clone().extract_tx();
Arc::new(tx.into())
}
// /// Combines this PartiallySignedTransaction with other PSBT as described by BIP 174.
// ///
// /// 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)]
pub struct OutPoint {
/// The referenced transaction's txid.
pub txid: String,
/// The index of the referenced output in its transaction's vout.
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::DescriptorSecretKey;
use crate::Network;
use bdk::bitcoin::bip32::Fingerprint;
use bdk::bitcoin::key::Secp256k1;
use bdk::bitcoin::Network;
use bdk::descriptor::{ExtendedDescriptor, IntoWalletDescriptor};
use bdk::keys::DescriptorPublicKey as BdkDescriptorPublicKey;
use bdk::keys::{DescriptorSecretKey as BdkDescriptorSecretKey, KeyMap};
@@ -12,9 +11,11 @@ use bdk::template::{
Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, Bip86, Bip86Public,
DescriptorTemplate,
};
use bdk::Error as BdkError;
use bdk::KeychainKind;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug)]
pub struct Descriptor {
@@ -23,9 +24,10 @@ pub struct 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 (extended_descriptor, key_map) = descriptor.into_wallet_descriptor(&secp, network)?;
let (extended_descriptor, key_map) =
descriptor.into_wallet_descriptor(&secp, network.into())?;
Ok(Self {
extended_descriptor,
key_map,
@@ -33,11 +35,11 @@ impl Descriptor {
}
pub(crate) fn new_bip44(
secret_key: &DescriptorSecretKey,
secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let derivable_key = &secret_key.0;
let derivable_key = &secret_key.inner;
match derivable_key {
BdkDescriptorSecretKey::Single(_) => {
@@ -45,8 +47,9 @@ impl Descriptor {
}
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip44(derivable_key, keychain_kind).build(network).unwrap();
let (extended_descriptor, key_map, _) = Bip44(derivable_key, keychain_kind)
.build(network.into())
.unwrap();
Self {
extended_descriptor,
key_map,
@@ -59,13 +62,13 @@ impl Descriptor {
}
pub(crate) fn new_bip44_public(
public_key: &DescriptorPublicKey,
public_key: Arc<DescriptorPublicKey>,
fingerprint: String,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.0;
let derivable_key = &public_key.inner;
match derivable_key {
BdkDescriptorPublicKey::Single(_) => {
@@ -75,7 +78,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip44Public(derivable_key, fingerprint, keychain_kind)
.build(network)
.build(network.into())
.unwrap();
Self {
@@ -90,11 +93,11 @@ impl Descriptor {
}
pub(crate) fn new_bip49(
secret_key: &DescriptorSecretKey,
secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let derivable_key = &secret_key.0;
let derivable_key = &secret_key.inner;
match derivable_key {
BdkDescriptorSecretKey::Single(_) => {
@@ -102,8 +105,9 @@ impl Descriptor {
}
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip49(derivable_key, keychain_kind).build(network).unwrap();
let (extended_descriptor, key_map, _) = Bip49(derivable_key, keychain_kind)
.build(network.into())
.unwrap();
Self {
extended_descriptor,
key_map,
@@ -116,13 +120,13 @@ impl Descriptor {
}
pub(crate) fn new_bip49_public(
public_key: &DescriptorPublicKey,
public_key: Arc<DescriptorPublicKey>,
fingerprint: String,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.0;
let derivable_key = &public_key.inner;
match derivable_key {
BdkDescriptorPublicKey::Single(_) => {
@@ -132,7 +136,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip49Public(derivable_key, fingerprint, keychain_kind)
.build(network)
.build(network.into())
.unwrap();
Self {
@@ -147,11 +151,11 @@ impl Descriptor {
}
pub(crate) fn new_bip84(
secret_key: &DescriptorSecretKey,
secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let derivable_key = &secret_key.0;
let derivable_key = &secret_key.inner;
match derivable_key {
BdkDescriptorSecretKey::Single(_) => {
@@ -159,8 +163,9 @@ impl Descriptor {
}
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip84(derivable_key, keychain_kind).build(network).unwrap();
let (extended_descriptor, key_map, _) = Bip84(derivable_key, keychain_kind)
.build(network.into())
.unwrap();
Self {
extended_descriptor,
key_map,
@@ -173,13 +178,13 @@ impl Descriptor {
}
pub(crate) fn new_bip84_public(
public_key: &DescriptorPublicKey,
public_key: Arc<DescriptorPublicKey>,
fingerprint: String,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.0;
let derivable_key = &public_key.inner;
match derivable_key {
BdkDescriptorPublicKey::Single(_) => {
@@ -189,7 +194,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip84Public(derivable_key, fingerprint, keychain_kind)
.build(network)
.build(network.into())
.unwrap();
Self {
@@ -204,11 +209,11 @@ impl Descriptor {
}
pub(crate) fn new_bip86(
secret_key: &DescriptorSecretKey,
secret_key: Arc<DescriptorSecretKey>,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let derivable_key = &secret_key.0;
let derivable_key = &secret_key.inner;
match derivable_key {
BdkDescriptorSecretKey::Single(_) => {
@@ -216,8 +221,9 @@ impl Descriptor {
}
BdkDescriptorSecretKey::XPrv(descriptor_x_key) => {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip86(derivable_key, keychain_kind).build(network).unwrap();
let (extended_descriptor, key_map, _) = Bip86(derivable_key, keychain_kind)
.build(network.into())
.unwrap();
Self {
extended_descriptor,
key_map,
@@ -230,13 +236,13 @@ impl Descriptor {
}
pub(crate) fn new_bip86_public(
public_key: &DescriptorPublicKey,
public_key: Arc<DescriptorPublicKey>,
fingerprint: String,
keychain_kind: KeychainKind,
network: Network,
) -> Self {
let fingerprint = Fingerprint::from_str(fingerprint.as_str()).unwrap();
let derivable_key = &public_key.0;
let derivable_key = &public_key.inner;
match derivable_key {
BdkDescriptorPublicKey::Single(_) => {
@@ -246,7 +252,7 @@ impl Descriptor {
let derivable_key = descriptor_x_key.xkey;
let (extended_descriptor, key_map, _) =
Bip86Public(derivable_key, fingerprint, keychain_kind)
.build(network)
.build(network.into())
.unwrap();
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)]
mod test {
use crate::*;
use assert_matches::assert_matches;
use bdk::descriptor::DescriptorError::Key;
use bdk::keys::KeyError::InvalidNetwork;
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();
DescriptorSecretKey::new(Network::Testnet, &mnemonic, None)
DescriptorSecretKey::new(Network::Testnet, Arc::new(mnemonic), None)
}
#[test]
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());
// tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h
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()
.as_public();
println!("Public 44: {}", handmade_public_44.as_string());
// Public 44: [d1d04177/44'/1'/0']tpubDCoPjomfTqh1e7o1WgGpQtARWtkueXQAepTeNpWiitS3Sdv8RKJ1yvTrGHcwjDXp2SKyMrTEca4LoN7gEUiGCWboyWe2rz99Kf4jK4m2Zmx/*
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()
.as_public();
println!("Public 49: {}", handmade_public_49.as_string());
// Public 49: [d1d04177/49'/1'/0']tpubDC65ZRvk1NDddHrVAUAZrUPJ772QXzooNYmPywYF9tMyNLYKf5wpKE7ZJvK9kvfG3FV7rCsHBNXy1LVKW95jrmC7c7z4hq7a27aD2sRrAhR/*
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()
.as_public();
println!("Public 84: {}", handmade_public_84.as_string());
// Public 84: [d1d04177/84'/1'/0']tpubDDNxbq17egjFk2edjv8oLnzxk52zny9aAYNv9CMqTzA4mQDiQq818sEkNe9Gzmd4QU8558zftqbfoVBDQorG3E4Wq26tB2JeE4KUoahLkx6/*
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()
.as_public();
println!("Public 86: {}", handmade_public_86.as_string());
// Public 86: [d1d04177/86'/1'/0']tpubDCJzjbcGbdEfXMWaL6QmgVmuSfXkrue7m2YNoacWwyc7a2XjXaKojRqNEbo41CFL3PyYmKdhwg2fkGpLX4SQCbQjCGxAkWHJTw9WEeenrJb/*
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 =
Descriptor::new_bip49(&master, KeychainKind::External, Network::Testnet);
Descriptor::new_bip49(master.clone(), KeychainKind::External, Network::Testnet);
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 =
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
println!("Template 49: {}", template_private_49.as_string());
println!("Template 44: {}", template_private_44.as_string());
println!("Template 84: {}", template_private_84.as_string());
println!("Template 86: {}", template_private_86.as_string());
let template_public_44 = Descriptor::new_bip44_public(
&handmade_public_44,
handmade_public_44,
"d1d04177".to_string(),
KeychainKind::External,
Network::Testnet,
);
let template_public_49 = Descriptor::new_bip49_public(
&handmade_public_49,
handmade_public_49,
"d1d04177".to_string(),
KeychainKind::External,
Network::Testnet,
);
let template_public_84 = Descriptor::new_bip84_public(
&handmade_public_84,
handmade_public_84,
"d1d04177".to_string(),
KeychainKind::External,
Network::Testnet,
);
let template_public_86 = Descriptor::new_bip86_public(
&handmade_public_86,
handmade_public_86,
"d1d04177".to_string(),
KeychainKind::External,
Network::Testnet,
@@ -390,8 +409,11 @@ mod test {
fn test_descriptor_from_string() {
let descriptor1 = Descriptor::new("wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEftEaNzz7dPGhWuKFU4VULesmhEfZYyBXdE/0/*)".to_string(), Network::Testnet);
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_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::error::EsploraError;
use crate::types::{FullScanRequest, SyncRequest};
use crate::wallet::Update;
use std::collections::BTreeMap;
use crate::wallet::{Update, Wallet};
use std::ops::Deref;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest;
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::wallet::Update as BdkUpdate;
use bdk::Error as BdkError;
use bdk_esplora::esplora_client::{BlockingClient, Builder};
use bdk_esplora::EsploraExt;
@@ -19,65 +12,57 @@ pub struct EsploraClient(BlockingClient);
impl EsploraClient {
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)
}
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,
request: Arc<FullScanRequest>,
wallet: Arc<Wallet>,
stop_gap: u64,
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: BdkFullScanRequest<KeychainKind> = request
) -> Result<Arc<Update>, BdkError> {
let wallet = wallet.get_wallet();
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
.lock()
.unwrap()
.take()
.ok_or(EsploraError::RequestAlreadyConsumed)?;
.scan_txs_with_keychains(
keychain_spks,
None,
None,
stop_gap as usize,
parallel_requests as usize,
)
.unwrap();
let result: BdkFullScanResult<KeychainKind> =
self.0
.full_scan(request, stop_gap as usize, parallel_requests as usize)?;
let missing_heights = update_graph.missing_heights(wallet.local_chain());
let chain_update = self
.0
.update_local_chain(previous_tip, missing_heights)
.unwrap();
let update = bdk::wallet::Update {
last_active_indices: result.last_active_indices,
graph: result.graph_update,
chain: Some(result.chain_update),
let update = BdkUpdate {
last_active_indices,
graph: update_graph,
chain: Some(chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn sync(
&self,
request: Arc<SyncRequest>,
parallel_requests: u64,
) -> Result<Arc<Update>, EsploraError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkSyncRequest = request
.0
.lock()
.unwrap()
.take()
.ok_or(EsploraError::RequestAlreadyConsumed)?;
// pub fn sync();
let result: BdkSyncResult = self.0.sync(request, parallel_requests as usize)?;
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();
pub fn broadcast(&self, transaction: Arc<crate::bitcoin::Transaction>) -> Result<(), BdkError> {
let bdk_transaction: bdk::bitcoin::Transaction = transaction.deref().clone().into();
self.0
.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::key::Secp256k1;
use bdk::bitcoin::secp256k1::rand;
use bdk::bitcoin::secp256k1::rand::Rng;
use bdk::bitcoin::Network;
use bdk::keys::bip39::WordCount;
use bdk::keys::bip39::{Language, Mnemonic as BdkMnemonic};
use bdk::keys::{
@@ -13,14 +12,20 @@ use bdk::keys::{
};
use bdk::miniscript::descriptor::{DescriptorXKey, Wildcard};
use bdk::miniscript::BareCtx;
use bdk::Error as BdkError;
use std::ops::Deref;
use std::str::FromStr;
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 {
/// Generates Mnemonic with a random entropy
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
let mut rng = rand::thread_rng();
@@ -30,23 +35,27 @@ impl Mnemonic {
let generated_key: GeneratedKey<_, BareCtx> =
BdkMnemonic::generate_with_entropy((word_count, Language::English), entropy).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)
.map(Mnemonic)
.map_err(Bip39Error::from)
.map(|m| Mnemonic { inner: m })
.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())
.map(Mnemonic)
.map_err(Bip39Error::from)
.map(|m| Mnemonic { inner: m })
.map_err(|e| BdkError::Generic(e.to_string()))
}
/// Returns Mnemonic as 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 {
pub(crate) fn new(path: String) -> Result<Self, Bip32Error> {
pub(crate) fn new(path: String) -> Result<Self, BdkError> {
BdkDerivationPath::from_str(&path)
.map(|x| DerivationPath {
inner_mutex: Mutex::new(x),
})
.map_err(Bip32Error::from)
.map_err(|e| BdkError::Generic(e.to_string()))
}
}
#[derive(Debug)]
pub struct DescriptorSecretKey(pub(crate) BdkDescriptorSecretKey);
pub struct DescriptorSecretKey {
pub(crate) inner: BdkDescriptorSecretKey,
}
impl DescriptorSecretKey {
pub(crate) fn new(network: Network, mnemonic: &Mnemonic, password: Option<String>) -> Self {
let mnemonic = mnemonic.0.clone();
pub(crate) fn new(network: Network, mnemonic: Arc<Mnemonic>, password: Option<String>) -> Self {
let mnemonic = mnemonic.inner.clone();
let xkey: ExtendedKey = (mnemonic, password).into_extended_key().unwrap();
let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey {
origin: None,
xkey: xkey.into_xprv(network).unwrap(),
xkey: xkey.into_xprv(network.into()).unwrap(),
derivation_path: BdkDerivationPath::master(),
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())
.map_err(DescriptorKeyError::from)?;
Ok(Self(descriptor_secret_key))
.map_err(|e| BdkError::Generic(e.to_string()))?;
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 descriptor_secret_key = &self.0;
let descriptor_secret_key = &self.inner;
let path = path.inner_mutex.lock().unwrap().deref().clone();
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) => {
let derived_xprv = descriptor_x_key
.xkey
.derive_priv(&secp, &path)
.map_err(DescriptorKeyError::from)?;
let derived_xprv = descriptor_x_key.xkey.derive_priv(&secp, &path)?;
let key_source = match descriptor_x_key.origin.clone() {
Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)),
None => (descriptor_x_key.xkey.fingerprint(&secp), path),
@@ -107,17 +121,23 @@ impl DescriptorSecretKey {
derivation_path: BdkDerivationPath::default(),
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> {
let descriptor_secret_key = &self.0;
pub(crate) fn extend(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let descriptor_secret_key = &self.inner;
let path = path.inner_mutex.lock().unwrap().deref().clone();
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) => {
let extended_path = descriptor_x_key.derivation_path.extend(path);
let extended_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey {
@@ -126,20 +146,27 @@ impl DescriptorSecretKey {
derivation_path: extended_path,
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> {
let secp = Secp256k1::new();
let descriptor_public_key = self.0.to_public(&secp).unwrap();
Arc::new(DescriptorPublicKey(descriptor_public_key))
let descriptor_public_key = self.inner.to_public(&secp).unwrap();
Arc::new(DescriptorPublicKey {
inner: descriptor_public_key,
})
}
/// Get the private key as bytes.
pub(crate) fn secret_bytes(&self) -> Vec<u8> {
let inner = &self.0;
let inner = &self.inner;
let secret_bytes: Vec<u8> = match inner {
BdkDescriptorSecretKey::Single(_) => {
unreachable!()
@@ -156,32 +183,35 @@ impl DescriptorSecretKey {
}
pub(crate) fn as_string(&self) -> String {
self.0.to_string()
self.inner.to_string()
}
}
#[derive(Debug)]
pub struct DescriptorPublicKey(pub(crate) BdkDescriptorPublicKey);
pub struct DescriptorPublicKey {
pub(crate) inner: BdkDescriptorPublicKey,
}
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())
.map_err(DescriptorKeyError::from)?;
Ok(Self(descriptor_public_key))
.map_err(|e| BdkError::Generic(e.to_string()))?;
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 descriptor_public_key = &self.0;
let descriptor_public_key = &self.inner;
let path = path.inner_mutex.lock().unwrap().deref().clone();
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) => {
let derived_xpub = descriptor_x_key
.xkey
.derive_pub(&secp, &path)
.map_err(DescriptorKeyError::from)?;
let derived_xpub = descriptor_x_key.xkey.derive_pub(&secp, &path)?;
let key_source = match descriptor_x_key.origin.clone() {
Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)),
None => (descriptor_x_key.xkey.fingerprint(), path),
@@ -192,17 +222,23 @@ impl DescriptorPublicKey {
derivation_path: BdkDerivationPath::default(),
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> {
let descriptor_public_key = &self.0;
pub(crate) fn extend(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> {
let descriptor_public_key = &self.inner;
let path = path.inner_mutex.lock().unwrap().deref().clone();
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) => {
let extended_path = descriptor_x_key.derivation_path.extend(path);
let extended_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey {
@@ -211,60 +247,67 @@ impl DescriptorPublicKey {
derivation_path: extended_path,
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 {
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)]
mod test {
use crate::keys::{DerivationPath, DescriptorPublicKey, DescriptorSecretKey, Mnemonic};
use crate::BdkError;
// use bdk::bitcoin::hashes::hex::ToHex;
use crate::error::DescriptorKeyError;
use bdk::bitcoin::Network;
use std::sync::Arc;
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();
DescriptorSecretKey::new(Network::Testnet, &mnemonic, None)
DescriptorSecretKey::new(Network::Testnet.into(), Arc::new(mnemonic), None)
}
fn derive_dsk(
key: &DescriptorSecretKey,
path: &str,
) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap();
key.derive(&path)
) -> Result<Arc<DescriptorSecretKey>, BdkError> {
let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.derive(path)
}
fn extend_dsk(
key: &DescriptorSecretKey,
path: &str,
) -> Result<Arc<DescriptorSecretKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap();
key.extend(&path)
) -> Result<Arc<DescriptorSecretKey>, BdkError> {
let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.extend(path)
}
fn derive_dpk(
key: &DescriptorPublicKey,
path: &str,
) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap();
key.derive(&path)
) -> Result<Arc<DescriptorPublicKey>, BdkError> {
let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.derive(path)
}
fn extend_dpk(
key: &DescriptorPublicKey,
path: &str,
) -> Result<Arc<DescriptorPublicKey>, DescriptorKeyError> {
let path = DerivationPath::new(path.to_string()).unwrap();
key.extend(&path)
) -> Result<Arc<DescriptorPublicKey>, BdkError> {
let path = Arc::new(DerivationPath::new(path.to_string()).unwrap());
key.extend(path)
}
#[test]
@@ -304,7 +347,7 @@ mod test {
assert_eq!(extended_dpk.as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/0/*");
let wif = "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch";
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);
assert!(result.is_err());
}

View File

@@ -1,58 +1,385 @@
mod bitcoin;
mod descriptor;
mod error;
mod esplora;
mod keys;
mod types;
mod wallet;
// TODO 6: Why are these imports required?
use crate::bitcoin::Address;
use crate::bitcoin::FeeRate;
use crate::bitcoin::Network;
use crate::bitcoin::OutPoint;
use crate::bitcoin::Psbt;
use crate::bitcoin::PartiallySignedTransaction;
use crate::bitcoin::Script;
use crate::bitcoin::Transaction;
use crate::bitcoin::TxOut;
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::keys::DerivationPath;
use crate::keys::DescriptorPublicKey;
use crate::keys::DescriptorSecretKey;
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::Update;
use crate::wallet::Wallet;
use bdk::bitcoin::Network;
use bdk::keys::bip39::WordCount;
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 std::sync::Arc;
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::error::{
CalculateFeeError, CannotConnectError, CreateTxError, PersistenceError, SignerError,
TxidParseError, WalletCreationError,
};
use crate::types::{
AddressInfo, Balance, CanonicalTx, FullScanRequest, LocalOutput, ScriptAmount, SyncRequest,
};
use crate::{AddressIndex, AddressInfo, Network, ScriptAmount};
use crate::{Balance, Script};
use std::collections::HashSet;
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::Network;
use bdk::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid};
use bdk::wallet::tx_builder::ChangeSpendPolicy;
use bdk::wallet::{ChangeSet, Update as BdkUpdate};
use bdk::Wallet as BdkWallet;
use bdk::{KeychainKind, SignOptions};
use bdk_file_store::Store;
use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::wallet::Update as BdkUpdate;
use bdk::{Error as BdkError, FeeRate};
use bdk::{SignOptions, Wallet as BdkWallet};
use std::collections::HashSet;
use std::str::FromStr;
use bdk::wallet::tx_builder::ChangeSpendPolicy;
use std::sync::{Arc, Mutex, MutexGuard};
const MAGIC_BYTES: &[u8] = "bdkffi".as_bytes();
#[derive(Debug)]
pub struct Wallet {
// TODO 8: Do we really need the mutex on the wallet? Could this be an Arc?
inner_mutex: Mutex<BdkWallet>,
}
impl Wallet {
pub fn new(
pub fn new_no_persist(
descriptor: Arc<Descriptor>,
change_descriptor: Option<Arc<Descriptor>>,
persistence_backend_path: String,
network: Network,
) -> Result<Self, WalletCreationError> {
) -> Result<Self, BdkError> {
let descriptor = descriptor.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 =
BdkWallet::new_or_load(&descriptor, change_descriptor.as_ref(), db, network)?;
let wallet =
BdkWallet::new_no_persist(&descriptor, change_descriptor.as_ref(), network.into())?;
Ok(Wallet {
inner_mutex: Mutex::new(wallet),
})
}
// TODO 10: Do we need this mutex
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet")
}
pub fn reveal_next_address(
&self,
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 get_address(&self, address_index: AddressIndex) -> AddressInfo {
self.get_wallet().get_address(address_index.into()).into()
}
pub fn network(&self) -> Network {
self.get_wallet().network()
self.get_wallet().network().into()
}
pub fn get_balance(&self) -> Balance {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance();
Balance::from(bdk_balance)
pub fn get_internal_address(&self, address_index: AddressIndex) -> AddressInfo {
self.get_wallet()
.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.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(
&self,
psbt: Arc<Psbt>,
psbt: Arc<PartiallySignedTransaction>,
// sign_options: Option<SignOptions>,
) -> Result<bool, SignerError> {
let mut psbt = psbt.0.lock().unwrap();
) -> Result<bool, BdkError> {
let mut psbt = psbt.inner.lock().unwrap();
self.get_wallet()
.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);
// /// 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)]
pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>,
@@ -165,11 +287,11 @@ pub struct TxBuilder {
pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) manually_selected_only: bool,
pub(crate) fee_rate: Option<FeeRate>,
pub(crate) fee_absolute: Option<u64>,
pub(crate) fee_rate: Option<f32>,
// pub(crate) fee_absolute: Option<u64>,
pub(crate) drain_wallet: bool,
pub(crate) drain_to: Option<BdkScriptBuf>,
pub(crate) rbf: Option<RbfValue>,
// pub(crate) drain_to: Option<BdkScript>,
// pub(crate) rbf: Option<RbfValue>,
// pub(crate) data: Vec<u8>,
}
@@ -182,15 +304,16 @@ impl TxBuilder {
change_policy: ChangeSpendPolicy::ChangeAllowed,
manually_selected_only: false,
fee_rate: None,
fee_absolute: None,
// fee_absolute: None,
drain_wallet: false,
drain_to: None,
rbf: None,
// drain_to: None,
// rbf: None,
// 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();
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> {
let mut unspendable_hash_set = self.unspendable.clone();
unspendable_hash_set.insert(unspendable);
@@ -220,17 +345,15 @@ impl TxBuilder {
})
}
pub(crate) fn unspendable(&self, unspendable: Vec<OutPoint>) -> Arc<Self> {
Arc::new(TxBuilder {
unspendable: unspendable.into_iter().collect(),
..self.clone()
})
}
/// Add an outpoint to the internal list of UTXOs that must be spent. 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_utxo(&self, outpoint: OutPoint) -> Arc<Self> {
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> {
let mut utxos = self.utxos.to_vec();
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> {
Arc::new(TxBuilder {
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> {
Arc::new(TxBuilder {
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> {
Arc::new(TxBuilder {
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 {
fee_rate: Some(fee_rate.clone()),
fee_rate: Some(sat_per_vb),
..self.clone()
})
}
pub(crate) fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> {
Arc::new(TxBuilder {
fee_absolute: Some(fee_amount),
..self.clone()
})
}
// /// Set an absolute fee.
// pub(crate) fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> {
// Arc::new(TxBuilder {
// 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> {
Arc::new(TxBuilder {
drain_wallet: true,
@@ -289,28 +428,52 @@ impl TxBuilder {
})
}
pub(crate) fn drain_to(&self, script: &Script) -> Arc<Self> {
Arc::new(TxBuilder {
drain_to: Some(script.0.clone()),
..self.clone()
})
}
// /// Sets the address to drain excess coins to. Usually, when there are excess coins they are sent to a change address
// /// generated by the wallet. This option replaces the usual change address with an arbitrary ScriptPubKey of your choosing.
// /// Just as with a change output, if the drain output is not needed (the excess coins are too small) it will not be included
// /// 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> {
Arc::new(TxBuilder {
rbf: Some(RbfValue::Default),
..self.clone()
})
}
/// Add data as an output using OP_RETURN.
// pub(crate) fn add_data(&self, data: Vec<u8>) -> Arc<Self> {
// Arc::new(TxBuilder {
// data,
// ..self.clone()
// })
// }
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 finish(&self, wallet: &Arc<Wallet>) -> Result<Arc<Psbt>, CreateTxError> {
/// Finish building the transaction. Returns the BIP174 PSBT.
pub(crate) fn finish(
&self,
wallet: &Wallet,
) -> Result<Arc<PartiallySignedTransaction>, BdkError> {
// 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 tx_builder = wallet.build_tx();
@@ -320,106 +483,137 @@ impl TxBuilder {
tx_builder.change_policy(self.change_policy);
if !self.utxos.is_empty() {
let bdk_utxos: Vec<BdkOutPoint> = self.utxos.iter().map(BdkOutPoint::from).collect();
tx_builder
.add_utxos(&bdk_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);
let utxos: &[BdkOutPoint] = &bdk_utxos;
tx_builder.add_utxos(utxos)?;
}
// 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 {
tx_builder.manually_selected_only();
}
if let Some(fee_rate) = &self.fee_rate {
tx_builder.fee_rate(fee_rate.0);
}
if let Some(fee_amount) = self.fee_absolute {
tx_builder.fee_absolute(fee_amount);
if let Some(sat_per_vb) = self.fee_rate {
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 self.drain_wallet {
tx_builder.drain_wallet();
}
if let Some(script) = &self.drain_to {
tx_builder.drain_to(script.clone());
}
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));
}
}
}
// if let Some(script) = &self.drain_to {
// tx_builder.drain_to(script.clone());
// }
// 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));
// }
// }
// }
// 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()))
}
}
#[derive(Clone)]
pub(crate) struct BumpFeeTxBuilder {
pub(crate) txid: String,
pub(crate) fee_rate: Arc<FeeRate>,
pub(crate) rbf: Option<RbfValue>,
}
impl BumpFeeTxBuilder {
pub(crate) fn new(txid: String, fee_rate: Arc<FeeRate>) -> Self {
Self {
txid,
fee_rate,
rbf: None,
}
}
pub(crate) fn enable_rbf(&self) -> Arc<Self> {
Arc::new(Self {
rbf: Some(RbfValue::Default),
..self.clone()
})
}
pub(crate) fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> {
Arc::new(Self {
rbf: Some(RbfValue::Value(nsequence)),
..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 {
outpoint: self.txid.clone(),
})?;
let mut wallet = wallet.get_wallet();
let mut tx_builder = wallet.build_fee_bump(txid).map_err(CreateTxError::from)?;
tx_builder.fee_rate(self.fee_rate.0);
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));
}
}
}
let psbt: BdkPsbt = tx_builder.finish()?;
Ok(Arc::new(psbt.into()))
}
}
#[derive(Clone, Debug)]
pub enum RbfValue {
Default,
Value(u32),
}
// /// The BumpFeeTxBuilder is used to bump the fee on a transaction that has been broadcast and has its RBF flag set to true.
// #[derive(Clone)]
// pub(crate) struct BumpFeeTxBuilder {
// pub(crate) txid: String,
// 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: f32) -> Self {
// Self {
// txid,
// fee_rate,
// allow_shrinking: None,
// rbf: None,
// }
// }
//
// /// 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
// /// cant be found among the recipients of the transaction we are bumping.
// pub(crate) fn allow_shrinking(&self, address: String) -> Arc<Self> {
// Arc::new(Self {
// allow_shrinking: Some(address),
// ..self.clone()
// })
// }
//
// /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`.
// pub(crate) fn enable_rbf(&self) -> Arc<Self> {
// Arc::new(Self {
// 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(Self {
// rbf: Some(RbfValue::Value(nsequence)),
// ..self.clone()
// })
// }
//
// /// Finish building the transaction. Returns the BIP174 PSBT.
// pub(crate) fn finish(
// &self,
// wallet: &Wallet,
// ) -> Result<Arc<PartiallySignedTransaction>, BdkError> {
// let wallet = wallet.get_wallet();
// let txid = Txid::from_str(self.txid.as_str())?;
// 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)]
// mod test {
// 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 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:

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
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
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
To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block:
```kotlin
@@ -29,17 +47,17 @@ dependencies {
* [Tatooine Faucet](https://github.com/thunderbiscuit/tatooine)
## How to build
_Note that Kotlin version `1.9.23` or later is required to build the library._
1. Install JDK 17. For example, with SDKMAN!:
_Note that Kotlin version `1.6.10` or later is required to build the library._
1. Install JDK 11. It must be version 11 (not 17), otherwise it won't build. For example, with SDKMAN!:
```shell
curl -s "https://get.sdkman.io" | bash
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
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.
```shell
@@ -57,7 +75,7 @@ rustup target add x86_64-apple-darwin aarch64-apple-darwin
## How to publish to your local Maven repo
```shell
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:
@@ -66,7 +84,7 @@ signing.gnupg.keyName=<YOUR_GNUPG_ID>
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
./gradlew publishToMavenLocal
```

View File

@@ -1,9 +1,4 @@
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"
}

View File

@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=true
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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
bdk-jvm/gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,11 +80,13 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
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.
MAX_FD=maximum
@@ -131,29 +133,22 @@ location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which java >/dev/null 2>&1 || 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
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
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 ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | 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" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,10 +193,6 @@ if "$cygwin" || "$msys" ; then
done
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;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@@ -214,12 +205,6 @@ set -- \
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.
#
# 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
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
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
plugins {
id("org.jetbrains.kotlin.jvm")
id("org.gradle.java-library")
id("org.gradle.maven-publish")
id("org.gradle.signing")
id("org.jetbrains.kotlin.jvm") version "1.6.10"
id("java-library")
id("maven-publish")
id("signing")
// Custom plugin to generate the native libs and bindings file
id("org.bitcoindevkit.plugins.generate-jvm-bindings")
}
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@@ -37,7 +41,7 @@ tasks.test {
testing {
suites {
val test by getting(JvmTestSuite::class) {
useKotlinTest("1.9.23")
useKotlinTest("1.6.10")
}
}
}
@@ -55,12 +59,10 @@ tasks.withType<Test> {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
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")
// testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
// testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1")
// testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
testImplementation("ch.qos.logback:logback-classic:1.2.3")
testImplementation("ch.qos.logback:logback-core:1.2.3")
}
@@ -107,10 +109,6 @@ afterEvaluate {
}
signing {
if (project.hasProperty("localBuild")) {
isRequired = false
}
val signingKeyId: String? by project
val signingKey: String? by project
val signingPassword: String? by project

View File

@@ -1,80 +1,29 @@
package org.bitcoindevkit
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
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 {
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
fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
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 psbt: Psbt = TxBuilder()
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2uL))
.feeRate(2.0f)
.finish(wallet)
println(psbt.serialize())
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
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test
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 {
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
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api")
// val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total()}")
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}")
}
assert(wallet.getBalance().total() > 0uL)
}
@Test
fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet: Wallet = Wallet(descriptor, null, 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}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()}")
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.scan(wallet, 10uL, 1uL)
assert(wallet.getBalance().total > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
wallet.applyUpdate(update)
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)
.feeRate(FeeRate.fromSatPerVb(2uL))
.feeRate(2.0f)
.finish(wallet)
println(psbt.serialize())
@@ -75,14 +46,8 @@ class LiveWalletTest {
assertTrue(walletDidSign)
val tx: Transaction = psbt.extractTx()
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)
}
}

View File

@@ -1,26 +1,10 @@
package org.bitcoindevkit
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
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
fun testDescriptorBip86() {
val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12)
@@ -36,18 +20,12 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet(
val wallet: Wallet = Wallet.newNoPersist(
descriptor,
null,
persistenceFilePath,
Network.TESTNET
)
val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network")
assertTrue(addressInfo.address.isValidForNetwork(Network.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")
val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New)
assertEquals(
expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e",
@@ -61,16 +39,15 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET
)
val wallet: Wallet = Wallet(
val wallet: Wallet = Wallet.newNoPersist(
descriptor,
null,
persistenceFilePath,
Network.TESTNET
)
assertEquals(
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 3: This will not work on mac Intel (x86_64 architecture)
val libraryPath = when (operatingSystem) {
OS.LINUX -> "./target/x86_64-unknown-linux-gnu/release-smaller/libbdkffi.so"
OS.MAC -> "./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib"
OS.WINDOWS -> "./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll"
else -> throw Exception("Unsupported OS")
}
// val libraryPath = when (operatingSystem) {
// OS.LINUX -> "./target/x86_64-unknown-linux-gnu/release-smaller/libbdkffi.so"
// OS.MAC -> "./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib"
// OS.WINDOWS -> "./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll"
// else -> throw Exception("Unsupported OS")
// }
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")
// 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")
// 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
// 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")
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")
executable("cargo")
args(cargoArgs)

View File

@@ -2,15 +2,3 @@ rootProject.name = "bdk-jvm"
include(":lib")
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}/pip install -r requirements.txt
echo "Generating bdk.py..."
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..."
rustup default 1.73.0
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..."
cp ./target/release-smaller/libbdkffi.so ../bdk-python/src/bdkpython/libbdkffi.so

View File

@@ -2,17 +2,16 @@
set -euo pipefail
python3 --version
pip install -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
pip install --user -r requirements.txt
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..."
cp ./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib

View File

@@ -2,17 +2,16 @@
set -euo pipefail
python3 --version
pip install -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
pip install --user -r requirements.txt
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..."
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
python3 --version
pip install -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
pip install --user -r requirements.txt
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..."
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
```python
import bdkpython as bdk
```
"""
setup(
name="bdkpython",
version="1.0.0a10.dev",
version="1.0.0a2.dev1",
description="The Python language bindings for the Bitcoin Development Kit",
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",

View File

@@ -1,93 +1,38 @@
import bdkpython as bdk
import unittest
import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
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")
class TestLiveTxBuilder(unittest.TestCase):
def test_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"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,
None,
"./bdk_persistence.db",
bdk.Network.SIGNET
bdk.Network.TESTNET
)
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
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.scan(
wallet = wallet,
stop_gap = 10,
parallel_requests = 1
)
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(wallet.get_balance().total(), 0)
recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET
)
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).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")
if __name__ == '__main__':
unittest.main()

View File

@@ -1,90 +1,62 @@
import bdkpython as bdk
import unittest
import os
SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
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")
class TestLiveWallet(unittest.TestCase):
def test_synced_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
wallet: bdk.Wallet = bdk.Wallet(
wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor,
None,
"./bdk_persistence.db",
bdk.Network.SIGNET
bdk.Network.TESTNET
)
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
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.scan(
wallet = wallet,
stop_gap = 10,
parallel_requests = 1
)
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):
descriptor: bdk.Descriptor = bdk.Descriptor(
"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,
None,
"./bdk_persistence.db",
bdk.Network.SIGNET
bdk.Network.TESTNET
)
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
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.scan(
wallet = wallet,
stop_gap = 10,
parallel_requests = 1
)
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(wallet.get_balance().total(), 0)
recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
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())
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
walletDidSign = wallet.sign(psbt)
self.assertTrue(walletDidSign)
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__':

View File

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

View File

@@ -1,46 +1,34 @@
import bdkpython as bdk
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):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
wallet: Wallet = bdk.Wallet(
wallet: Wallet = bdk.Wallet.new_no_persist(
descriptor,
None,
"./bdk_persistence.db",
bdk.Network.TESTNET
)
address_info: bdk.AddressInfo = wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL)
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network")
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.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")
address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW())
self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string())
def test_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET
)
wallet: bdk.Wallet = bdk.Wallet(
wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor,
None,
"./bdk_persistence.db",
bdk.Network.TESTNET
)
self.assertEqual(wallet.get_balance().total, 0)
self.assertEqual(wallet.get_balance().total(), 0)
if __name__ == '__main__':
unittest.main()

View File

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

View File

@@ -1,107 +1,34 @@
import XCTest
@testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
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 {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
network: Network.testnet
)
let wallet = try Wallet(
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path,
network: .signet
network: .testnet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.scan(
wallet: wallet,
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")
XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0), "Wallet must have positive balance, please add funds")
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try TxBuilder()
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: PartiallySignedTransaction = try TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.feeRate(satPerVbyte: 2.0)
.finish(wallet: wallet)
print(psbt.serialize())
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,109 +1,65 @@
import XCTest
@testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
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 {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
network: Network.testnet
)
let wallet = try Wallet(
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path,
network: .signet
network: .testnet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.scan(
wallet: wallet,
stopGap: 10,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
try wallet.commit()
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)")
}
XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0))
}
func testBroadcastTransaction() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
network: Network.testnet
)
let wallet = try Wallet(
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path,
network: .signet
network: .testnet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.scan(
wallet: wallet,
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")
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 psbt: Psbt = try
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: PartiallySignedTransaction = try
TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.feeRate(satPerVbyte: 2.0)
.finish(wallet: wallet)
print(psbt.serialize())
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
let walletDidSign: Bool = try wallet.sign(psbt: psbt)
XCTAssertTrue(walletDidSign, "Wallet did not sign transaction")
let tx: Transaction = try! psbt.extractTx()
let tx: Transaction = psbt.extractTx()
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)
}
}

View File

@@ -2,49 +2,18 @@ import XCTest
@testable import BitcoinDevKit
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 {
let descriptor: Descriptor = try Descriptor(
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet
)
let wallet = try Wallet(
let wallet: Wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path,
network: .testnet
)
let addressInfo: AddressInfo = try wallet.revealNextAddress(keychain: KeychainKind.external)
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")
let addressInfo: AddressInfo = wallet.getAddress(addressIndex: AddressIndex.new)
XCTAssertEqual(addressInfo.address.asString(), "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e")
}
@@ -53,13 +22,12 @@ final class OfflineWalletTests: XCTestCase {
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet
)
let wallet = try Wallet(
let wallet: Wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
persistenceBackendPath: dbFilePath.path,
network: .testnet
)
XCTAssertEqual(wallet.getBalance().total, 0)
XCTAssertEqual(wallet.getBalance().total(), 0)
}
}

View File

@@ -16,8 +16,6 @@
</array>
<key>SupportedPlatform</key>
<string>macos</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
@@ -33,8 +31,6 @@
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
@@ -47,8 +43,6 @@
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
</dict>
</array>
<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
# to your application pointing at the bdk-swift directory.
rustup default 1.77.1
rustup component add rust-src
rustup target add aarch64-apple-ios # iOS arm64
rustup target add x86_64-apple-ios # iOS x86_64
rustup target add aarch64-apple-ios-sim # simulator mac M1
rustup target add aarch64-apple-darwin # mac M1
rustup target add x86_64-apple-darwin # mac x86_64
# Run the script from the repo root directory, ie: ./bdk-swift/build-local-swift.sh
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 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-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
lipo target/aarch64-apple-ios-sim/release-smaller/libbdkffi.a target/x86_64-apple-ios/release-smaller/libbdkffi.a -create -output target/lipo-ios-sim/release-smaller/libbdkffi.a
mkdir -p target/lipo-macos/release-smaller
lipo target/aarch64-apple-darwin/release-smaller/libbdkffi.a target/x86_64-apple-darwin/release-smaller/libbdkffi.a -create -output target/lipo-macos/release-smaller/libbdkffi.a
cd ../bdk-swift/ || exit
popd
pushd bdk-swift
mv Sources/BitcoinDevKit/bdk.swift Sources/BitcoinDevKit/BitcoinDevKit.swift
cp Sources/BitcoinDevKit/bdkFFI.h bdkFFI.xcframework/ios-arm64/bdkFFI.framework/Headers
cp Sources/BitcoinDevKit/bdkFFI.h bdkFFI.xcframework/ios-arm64_x86_64-simulator/bdkFFI.framework/Headers
@@ -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
rm Sources/BitcoinDevKit/bdkFFI.h
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