├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── enhancement_request.md │ ├── release.md │ └── summer_project.md ├── pull_request_template.md └── workflows │ ├── audit.yml │ ├── cont_integration.yml │ ├── kotlin-api-docs.yaml │ ├── live-tests.yaml │ ├── publish-android.yaml │ ├── publish-jvm.yaml │ ├── publish-python.yaml │ ├── test-android.yaml │ ├── test-jvm.yaml │ ├── test-python.yaml │ ├── test-swift.yaml │ └── zizmor.yml ├── .gitignore ├── CHANGELOG.md ├── DEVELOPMENT_CYCLE.md ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── PGP-BDK-BINDINGS.asc ├── README.md ├── bdk-android ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── justfile ├── lib │ ├── README.md │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ ├── assets │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── org │ │ │ └── bitcoindevkit │ │ │ ├── LiveTxBuilderTest.kt │ │ │ ├── LiveWalletTest.kt │ │ │ ├── OfflineDescriptorTest.kt │ │ │ └── OfflineWalletTest.kt │ │ └── main │ │ └── AndroidManifest.xml ├── scripts │ ├── build-linux-x86_64.sh │ ├── build-macos-aarch64.sh │ └── build-windows-x86_64.sh └── settings.gradle.kts ├── bdk-ffi ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── justfile ├── src │ ├── bdk.udl │ ├── bitcoin.rs │ ├── descriptor.rs │ ├── electrum.rs │ ├── error.rs │ ├── esplora.rs │ ├── keys.rs │ ├── kyoto.rs │ ├── lib.rs │ ├── macros.rs │ ├── store.rs │ ├── tx_builder.rs │ ├── types.rs │ └── wallet.rs ├── tests │ ├── README.md │ ├── bindings │ │ ├── test.kts │ │ ├── test.py │ │ └── test.swift │ ├── jna │ │ └── jna-5.14.0.jar │ └── test_generated_bindings.rs ├── uniffi-android.toml ├── uniffi-bindgen.rs └── uniffi.toml ├── bdk-jvm ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── justfile ├── lib │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── test │ │ ├── kotlin │ │ └── org │ │ │ └── bitcoindevkit │ │ │ ├── LiveElectrumClientTest.kt │ │ │ ├── LiveKyotoTest.kt │ │ │ ├── LiveMemoryWalletTest.kt │ │ │ ├── LiveTransactionTest.kt │ │ │ ├── LiveTxBuilderTest.kt │ │ │ ├── LiveWalletTest.kt │ │ │ ├── OfflineDescriptorTest.kt │ │ │ ├── OfflinePersistenceTest.kt │ │ │ └── OfflineWalletTest.kt │ │ └── resources │ │ └── pre_existing_wallet_persistence_test.sqlite ├── scripts │ ├── build-linux-x86_64.sh │ ├── build-macos-aarch64.sh │ ├── build-macos-x86_64.sh │ └── build-windows-x86_64.sh └── settings.gradle.kts ├── bdk-python ├── .gitignore ├── MANIFEST.in ├── README.md ├── justfile ├── nix │ ├── uniffi_0.14.1_cargo_lock.patch │ ├── uniffi_0.15.2_cargo_lock.patch │ └── uniffi_bindgen.nix ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── scripts │ ├── generate-linux.sh │ ├── generate-macos-arm64.sh │ ├── generate-macos-x86_64.sh │ └── generate-windows.sh ├── setup.py ├── shell.nix ├── src │ └── bdkpython │ │ └── __init__.py └── tests │ ├── __init__.py │ ├── test_live_kyoto.py │ ├── test_live_tx_builder.py │ ├── test_live_wallet.py │ ├── test_offline_custom_persist.py │ ├── test_offline_descriptor.py │ └── test_offline_wallet.py ├── bdk-swift ├── Package.swift ├── Package.swift.txt ├── README.md ├── Tests │ └── BitcoinDevKitTests │ │ ├── LiveElectrumClientTests.swift │ │ ├── LiveKyotoTests.swift │ │ ├── LiveMemoryWalletTests.swift │ │ ├── LiveTransactionTests.swift │ │ ├── LiveTxBuilderTests.swift │ │ ├── LiveWalletTests.swift │ │ ├── OfflineDescriptorTests.swift │ │ ├── OfflinePersistenceTests.swift │ │ ├── OfflineWalletTests.swift │ │ └── Resources │ │ └── pre_existing_wallet_persistence_test.sqlite ├── build-xcframework.sh └── justfile └── docs └── adr ├── 01-naming.md ├── 02-wrapping.md ├── 03-errrors.md └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /bdk-jvm/ @thunderbiscuit 2 | /bdk-android/ @thunderbiscuit 3 | /bdk-swift/ @reez 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Build environment** 20 | - BDK tag/commit: 21 | - OS+version: 22 | - Rust/Cargo version: 23 | - Rust/Cargo target: 24 | 25 | **Additional context** 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Request a new feature or change to an existing feature 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the enhancement** 11 | 12 | 13 | **Use case** 14 | 15 | 16 | **Additional context** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | about: Create a new release [for release managers only] 4 | title: 'Release MAJOR.MINOR.PATCH' 5 | labels: 'release' 6 | assignees: '' 7 | --- 8 | 9 | # Part 1: Bump BDK Rust Version 10 | 11 | 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. 12 | 2. - [ ] Once the new bdk release is out, update the PR to replace the release candidate with the full release and merge. 13 | 14 | # Part 2: Prepare Libraries for Release Branch 15 | 16 | ### _Android_ 17 | 18 | 3. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root and `lib`) in the bdk-android directory to make sure you're building the library from scratch. 19 | 4. - [ ] Build the library and run the offline and live tests, and adjust them if necessary (note that you'll need an Android emulator running). 20 | ```shell 21 | # start an emulator prior to running the tests 22 | cd ./bdk-android/ 23 | just clean 24 | just build-macos 25 | just test 26 | ``` 27 | 5. - [ ] Update the readme if necessary. 28 | 29 | ### _JVM_ 30 | 31 | 6. - [ ] Delete the `target` directory in bdk-ffi and all `build` directories (in root and `lib`) in bdk-jvm directory to make sure you're building the library from scratch. 32 | 7. - [ ] Build the library and run all offline and live tests, and adjust them if necessary. 33 | ```shell 34 | cd ./bdk-jvm/ 35 | just clean 36 | just build 37 | just test 38 | ``` 39 | 8. - [ ] Update the readme if necessary. 40 | 41 | ### _Swift_ 42 | 43 | 9. - [ ] Delete the `target` directory in bdk-ffi. 44 | 10. - [ ] Run all offline and live tests and adjust them if necessary. 45 | ```shell 46 | cd ./bdk-swift/ 47 | just clean 48 | just build 49 | just test 50 | ``` 51 | 11. - [ ] Update the readme if necessary. 52 | 53 | ### _Python_ 54 | 55 | 12. - [ ] 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. 56 | 13. - [ ] Build the library. 57 | ```shell 58 | cd ./bdk-python/ 59 | just clean 60 | pip3 install --requirement requirements.txt 61 | bash ./scripts/generate-macos-arm64.sh # run the script for your particular platform 62 | python3 setup.py --verbose bdist_wheel 63 | ``` 64 | 14. - [ ] Run all offline and live tests and adjust them if necessary. 65 | ```shell 66 | pip3 install ./dist/bdkpython--py3-none-any.whl --force-reinstall 67 | python -m unittest --verbose 68 | ``` 69 | 15. - [ ] Update the readme and `setup.py` if necessary. 70 | 71 | ## Part 3: Release Workflow 72 | 73 | 16. - [ ] Update the Android, JVM, Python, and Swift libraries as per the _specific libraries' workflows_ sections 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). 74 | 17. - [ ] Create a new branch off of `master` called `release/`, e.g. `release/1.2` 75 | 18. - [ ] Update bdk-android version from `SNAPSHOT` version to release version 76 | 19. - [ ] Update bdk-jvm version from `SNAPSHOT` version to release version 77 | 20. - [ ] Update bdk-python version from `.dev` version to release version 78 | 21. - [ ] 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). 79 | 22. - [ ] Get a review and ACK and merge the PR updating all the languages to their release versions on the release branch. 80 | 23. - [ ] 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. 81 | ```shell 82 | git tag v0.6.0 --sign --edit 83 | git push upstream v0.6.0 84 | ``` 85 | 24. - [ ] Trigger manual releases for all 4 libraries (for Swift, go on the [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) repository, and trigger the using `master`. 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`). Note also that for all 3 other libraries on the bdk-ffi repo, you should trigger the release workflow using the tag (not a branch). 86 | 25. - [ ] Make sure the released libraries work and contain the artifacts you would expect. 87 | 26. - [ ] Aggregate all the changelog notices from the PRs and add them to the changelog file. PR that. 88 | 27. - [ ] Bump the versions on master from `1.1.0-SNAPSHOT` to `1.2.0-SNAPSHOT` (Android + JVM), `1.1.0.dev0` to `1.2.0.dev0` (Python), and `1.1.0-dev` to `1.2.0-alpha.0` (Rust). 89 | 28. - [ ] Apply changes to the release issue template if needed. 90 | 29. - [ ] Make release on GitHub (generate auto release notes between the previous tag and the new one). 91 | 30. - [ ] Build API docs for Android and JVM locally and PR the websites to the bitcoindevkit.org repo. 92 | 31. - [ ] Post in the announcement channel. 93 | 32. - [ ] Tweet about the new release! 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/summer_project.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Summer of Bitcoin Project 3 | about: Template to suggest a new https://www.summerofbitcoin.org/ project. 4 | title: '' 5 | labels: 'summer-of-bitcoin' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 34 | 35 | **Description** 36 | 37 | 38 | **Expected Outcomes** 39 | 40 | 41 | **Resources** 42 | 43 | 44 | 45 | 46 | 47 | 48 | **Skills Required** 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | **Mentor(s)** 57 | 58 | 59 | **Difficulty** 60 | 61 | 62 | **Competency Test (optional)** 63 | 65 | 66 | 69 | 72 | 74 | 76 | 78 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### Notes to the reviewers 8 | 9 | 11 | 12 | ### Changelog notice 13 | 14 | 15 | 16 | 17 | ### Checklists 18 | 19 | #### All Submissions: 20 | 21 | * [ ] I've signed all my commits 22 | * [ ] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) 23 | * [ ] I ran `cargo fmt` and `cargo clippy` before committing 24 | 25 | #### New Features: 26 | 27 | * [ ] I've added tests for the new feature 28 | * [ ] I've added docs for the new feature 29 | 30 | #### Bugfixes: 31 | 32 | * [ ] This pull request breaks the existing API 33 | * [ ] I've added tests to reproduce the issue which are now passing 34 | * [ ] I'm linking the issue being fixed by this PR 35 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**/Cargo.toml' 7 | - '**/Cargo.lock' 8 | schedule: 9 | - cron: '0 0 * * 0' # Once per week 10 | 11 | jobs: 12 | security_audit: 13 | name: Security audit 14 | runs-on: ubuntu-24.04 15 | defaults: 16 | run: 17 | working-directory: bdk-ffi 18 | steps: 19 | - name: "Check out PR branch" 20 | uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | 24 | - name: "Run audit" 25 | run: | 26 | cargo install cargo-audit 27 | cargo-audit audit 28 | -------------------------------------------------------------------------------- /.github/workflows/cont_integration.yml: -------------------------------------------------------------------------------- 1 | name: Rust layer CI 2 | on: 3 | push: 4 | paths: 5 | - "bdk-ffi/**" 6 | pull_request: 7 | paths: 8 | - "bdk-ffi/**" 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | build-test: 14 | name: "Build and test" 15 | runs-on: ubuntu-24.04 16 | defaults: 17 | run: 18 | working-directory: bdk-ffi 19 | strategy: 20 | matrix: 21 | rust: 22 | - version: 1.84.1 23 | clippy: true 24 | steps: 25 | - name: "Checkout" 26 | uses: actions/checkout@v4 27 | with: 28 | persist-credentials: false 29 | 30 | - name: "Generate cache key" 31 | env: 32 | MATRIX_RUST_VERSION: ${{ matrix.rust.version }} 33 | MATRIX_FEATURES: ${{ matrix.features }} 34 | run: echo "$MATRIX_RUST_VERSION $MATRIX_FEATURES" | tee .cache_key 35 | 36 | - name: "Cache" 37 | uses: actions/cache@v3 38 | with: 39 | path: | 40 | ~/.cargo/registry 41 | ~/.cargo/git 42 | target 43 | key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 44 | 45 | - name: "Set default toolchain" 46 | env: 47 | MATRIX_RUST_VERSION: ${{ matrix.rust.version }} 48 | run: rustup default $MATRIX_RUST_VERSION 49 | 50 | - name: "Set profile" 51 | run: rustup set profile minimal 52 | 53 | - name: "Add clippy" 54 | if: ${{ matrix.rust.clippy }} 55 | run: rustup component add clippy 56 | 57 | - name: "Update toolchain" 58 | run: rustup update 59 | 60 | - name: "Build" 61 | run: cargo build 62 | 63 | - name: "Clippy" 64 | if: ${{ matrix.rust.clippy }} 65 | run: cargo clippy --all-targets --features "uniffi/bindgen-tests" 66 | 67 | - name: "Test" 68 | run: CLASSPATH=./tests/jna/jna-5.14.0.jar cargo test --features uniffi/bindgen-tests 69 | 70 | fmt: 71 | name: "Rust fmt" 72 | runs-on: ubuntu-24.04 73 | defaults: 74 | run: 75 | working-directory: bdk-ffi 76 | steps: 77 | - name: "Checkout" 78 | uses: actions/checkout@v4 79 | with: 80 | persist-credentials: false 81 | 82 | - name: "Set default toolchain" 83 | run: rustup default nightly 84 | 85 | - name: "Set profile" 86 | run: rustup set profile minimal 87 | 88 | - name: "Add rustfmt" 89 | run: rustup component add rustfmt 90 | 91 | - name: "Update toolchain" 92 | run: rustup update 93 | 94 | - name: "Check fmt" 95 | run: cargo fmt --all -- --config format_code_in_doc_comments=true --check 96 | -------------------------------------------------------------------------------- /.github/workflows/kotlin-api-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build JVM and Android API Docs Websites 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: {} 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - name: "Checkout" 12 | uses: actions/checkout@v3 13 | with: 14 | persist-credentials: false 15 | 16 | - name: "Set up JDK 17" 17 | uses: actions/setup-java@v2 18 | with: 19 | distribution: temurin 20 | java-version: 17 21 | 22 | - name: "Build JVM API documentation" 23 | run: | 24 | cd ./bdk-jvm/ 25 | bash ./scripts/build-linux-x86_64.sh 26 | ./gradlew dokkaHtml 27 | 28 | - name: "Upload JVM website" 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: artifact-jvm-api-docs 32 | path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-jvm/lib/build/dokka/html/ 33 | 34 | - name: "Build Android API documentation" 35 | run: | 36 | cd ./bdk-android/ 37 | bash ./scripts/build-linux-x86_64.sh 38 | ./gradlew dokkaHtml 39 | 40 | - name: "Upload Android website" 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: artifact-android-api-docs 44 | path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-android/lib/build/dokka/html/ 45 | -------------------------------------------------------------------------------- /.github/workflows/live-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run All Live Tests 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * 0' # Once per week 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | jvm-tests: 11 | name: "Build and test JVM library on Linux" 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - name: "Checkout publishing branch" 15 | uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | 19 | - name: "Cache" 20 | uses: actions/cache@v3 21 | with: 22 | path: | 23 | ~/.cargo/registry 24 | ~/.cargo/git 25 | ./target 26 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 27 | 28 | - name: "Set up JDK" 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 11 33 | 34 | - name: "Set default Rust version to 1.84.1" 35 | run: rustup default 1.84.1 36 | 37 | - name: "Build library and run tests" 38 | run: | 39 | cd bdk-jvm 40 | bash ./scripts/build-linux-x86_64.sh 41 | ./gradlew test 42 | 43 | swift-tests: 44 | name: "Build and test iOS library on macOS" 45 | runs-on: macos-14 46 | steps: 47 | - name: "Checkout" 48 | uses: actions/checkout@v4 49 | with: 50 | persist-credentials: false 51 | 52 | - name: "Build Swift package" 53 | working-directory: bdk-swift 54 | run: bash ./build-xcframework.sh 55 | 56 | - name: "Run live Swift tests" 57 | working-directory: bdk-swift 58 | run: swift test 59 | 60 | python-tests: 61 | name: "Build and test Python library on Linux" 62 | runs-on: ubuntu-24.04 63 | defaults: 64 | run: 65 | working-directory: bdk-python 66 | container: 67 | image: quay.io/pypa/manylinux_2_28_x86_64 68 | env: 69 | PLAT: manylinux_2_28_x86_64 70 | PYBIN: "/opt/python/${{ matrix.python }}/bin" 71 | strategy: 72 | matrix: 73 | python: 74 | - cp310-cp310 75 | steps: 76 | - name: "Checkout" 77 | uses: actions/checkout@v4 78 | with: 79 | submodules: true 80 | persist-credentials: false 81 | 82 | - name: "Install Rust 1.84.1" 83 | uses: actions-rs/toolchain@v1 84 | with: 85 | toolchain: 1.84.1 86 | 87 | - name: "Generate bdk.py and binaries" 88 | run: bash ./scripts/generate-linux.sh 89 | 90 | - name: "Build wheel" 91 | # Specifying the plat-name argument is necessary to build a wheel with the correct name, 92 | # see issue #350 for more information 93 | run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_28_x86_64 --verbose 94 | 95 | - name: "Install wheel" 96 | run: ${PYBIN}/pip install ./dist/*.whl 97 | 98 | - name: "Run live Python tests" 99 | run: ${PYBIN}/python -m unittest --verbose 100 | -------------------------------------------------------------------------------- /.github/workflows/publish-android.yaml: -------------------------------------------------------------------------------- 1 | name: Publish bdk-android to Maven Central 2 | on: [workflow_dispatch] 3 | 4 | # The default Android NDK on the ubuntu-24.04 image is 25.2.9519653 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: "Check out PR branch" 13 | uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | 17 | - name: "Cache" 18 | uses: actions/cache@v3 19 | with: 20 | path: | 21 | ~/.cargo/registry 22 | ~/.cargo/git 23 | ./target 24 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 25 | 26 | - name: "Set up JDK" 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: temurin 30 | java-version: 17 31 | 32 | - name: "Build bdk-android library" 33 | run: | 34 | cd bdk-android 35 | bash ./scripts/build-linux-x86_64.sh 36 | 37 | - name: "Publish to Maven Central" 38 | env: 39 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} 40 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} 41 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} 42 | ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} 43 | ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} 44 | run: | 45 | cd bdk-android 46 | ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-jvm.yaml: -------------------------------------------------------------------------------- 1 | name: Publish bdk-jvm to Maven Central 2 | on: [workflow_dispatch] 3 | 4 | permissions: {} 5 | 6 | jobs: 7 | build-macOS-native-libs: 8 | name: "Create M1 and x86_64 native binaries" 9 | runs-on: macos-14 10 | steps: 11 | - name: "Checkout publishing branch" 12 | uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | 16 | - name: "Cache" 17 | uses: actions/cache@v3 18 | with: 19 | path: | 20 | ~/.cargo/registry 21 | ~/.cargo/git 22 | ./target 23 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 24 | 25 | - name: "Set up JDK" 26 | uses: actions/setup-java@v4 27 | with: 28 | distribution: temurin 29 | java-version: 17 30 | 31 | - name: "Build bdk-jvm library" 32 | run: | 33 | cd bdk-jvm 34 | bash ./scripts/build-macos-aarch64.sh 35 | bash ./scripts/build-macos-x86_64.sh 36 | 37 | - name: "Upload macOS native libraries for reuse in publishing job" 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: artifact-macos 41 | path: /Users/runner/work/bdk-ffi/bdk-ffi/bdk-jvm/lib/src/main/resources/ 42 | 43 | build-windows-native-lib: 44 | name: "Create Windows native binaries" 45 | runs-on: windows-2022 46 | steps: 47 | - name: "Checkout publishing branch" 48 | uses: actions/checkout@v4 49 | with: 50 | persist-credentials: false 51 | 52 | - name: "Set up JDK" 53 | uses: actions/setup-java@v4 54 | with: 55 | distribution: temurin 56 | java-version: 17 57 | 58 | - name: "Build bdk-jvm library" 59 | run: | 60 | cd bdk-jvm 61 | bash ./scripts/build-windows-x86_64.sh 62 | 63 | - name: "Upload Windows native libraries for reuse in publishing job" 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: artifact-windows 67 | path: D:\a\bdk-ffi\bdk-ffi\bdk-jvm\lib\src\main\resources\ 68 | 69 | build-full-library: 70 | name: Create full bdk-jvm library 71 | needs: [build-macOS-native-libs, build-windows-native-lib] 72 | runs-on: ubuntu-24.04 73 | steps: 74 | - name: "Checkout publishing branch" 75 | uses: actions/checkout@v4 76 | with: 77 | persist-credentials: false 78 | 79 | - name: "Cache" 80 | uses: actions/cache@v3 81 | with: 82 | path: | 83 | ~/.cargo/registry 84 | ~/.cargo/git 85 | ./target 86 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 87 | 88 | - name: "Set up JDK" 89 | uses: actions/setup-java@v4 90 | with: 91 | distribution: temurin 92 | java-version: 17 93 | 94 | - name: "Build bdk-jvm library" 95 | run: | 96 | cd bdk-jvm 97 | bash ./scripts/build-linux-x86_64.sh 98 | 99 | - name: "Download macOS native binaries from previous job" 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: artifact-macos 103 | path: ./bdk-jvm/lib/src/main/resources/ 104 | 105 | - name: "Download Windows native libraries from previous job" 106 | uses: actions/download-artifact@v4 107 | with: 108 | name: artifact-windows 109 | path: ./bdk-jvm/lib/src/main/resources/ 110 | 111 | - name: "Upload library code and binaries" 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: artifact-full 115 | path: ./bdk-jvm/lib/ 116 | 117 | - name: "Publish to Maven Central" 118 | env: 119 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} 120 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} 121 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} 122 | ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} 123 | ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} 124 | run: | 125 | cd bdk-jvm 126 | ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 127 | -------------------------------------------------------------------------------- /.github/workflows/publish-python.yaml: -------------------------------------------------------------------------------- 1 | name: Publish bdkpython to PyPI 2 | on: [workflow_dispatch] 3 | 4 | permissions: {} 5 | 6 | jobs: 7 | build-manylinux_2_28-x86_64-wheels: 8 | name: "Build Manylinux 2.28 x86_64 wheel" 9 | runs-on: ubuntu-24.04 10 | defaults: 11 | run: 12 | working-directory: bdk-python 13 | container: 14 | image: quay.io/pypa/manylinux_2_28_x86_64 15 | env: 16 | PLAT: manylinux_2_28_x86_64 17 | PYBIN: "/opt/python/${{ matrix.python }}/bin" 18 | strategy: 19 | matrix: 20 | python: # Update this list whenever the docker image is updated (check /opt/python/) 21 | - cp38-cp38 22 | - cp39-cp39 23 | - cp310-cp310 24 | - cp311-cp311 25 | - cp312-cp312 26 | steps: 27 | - name: "Checkout" 28 | uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | persist-credentials: false 32 | 33 | - name: "Install Rust 1.84.1" 34 | uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: 1.84.1 37 | 38 | - name: "Generate bdk.py and binaries" 39 | run: bash ./scripts/generate-linux.sh 40 | 41 | - name: "Build wheel" 42 | # Specifying the plat-name argument is necessary to build a wheel with the correct name, 43 | # see issue #350 for more information 44 | run: ${PYBIN}/python setup.py bdist_wheel --plat-name manylinux_2_28_x86_64 --verbose 45 | 46 | - uses: actions/upload-artifact@v4 47 | with: 48 | name: bdkpython-manylinux_2_28_x86_64-${{ matrix.python }} 49 | path: /home/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl 50 | 51 | build-macos-arm64-wheels: 52 | name: "Build macOS arm64 wheel" 53 | runs-on: macos-13 54 | defaults: 55 | run: 56 | working-directory: bdk-python 57 | strategy: 58 | matrix: 59 | python: 60 | - "3.8" 61 | - "3.9" 62 | - "3.10" 63 | - "3.11" 64 | - "3.12" 65 | steps: 66 | - name: "Checkout" 67 | uses: actions/checkout@v4 68 | with: 69 | submodules: true 70 | persist-credentials: false 71 | 72 | - name: "Install Python" 73 | uses: actions/setup-python@v4 74 | with: 75 | python-version: ${{ matrix.python }} 76 | 77 | - name: "Generate bdk.py and binaries" 78 | run: bash ./scripts/generate-macos-arm64.sh 79 | 80 | - name: "Build wheel" 81 | # Specifying the plat-name argument is necessary to build a wheel with the correct name, 82 | # see issue #350 for more information 83 | run: python3 setup.py bdist_wheel --plat-name macosx_11_0_arm64 --verbose 84 | 85 | - name: "Upload artifacts" 86 | uses: actions/upload-artifact@v4 87 | with: 88 | name: bdkpython-macos-arm64-${{ matrix.python }} 89 | path: /Users/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl 90 | 91 | build-macos-x86_64-wheels: 92 | name: "Build macOS x86_64 wheel" 93 | runs-on: macos-13 94 | defaults: 95 | run: 96 | working-directory: bdk-python 97 | strategy: 98 | matrix: 99 | python: 100 | - "3.8" 101 | - "3.9" 102 | - "3.10" 103 | - "3.11" 104 | - "3.12" 105 | steps: 106 | - name: "Checkout" 107 | uses: actions/checkout@v4 108 | with: 109 | submodules: true 110 | persist-credentials: false 111 | 112 | - name: "Install Python" 113 | uses: actions/setup-python@v4 114 | with: 115 | python-version: ${{ matrix.python }} 116 | 117 | - name: "Generate bdk.py and binaries" 118 | run: bash ./scripts/generate-macos-x86_64.sh 119 | 120 | - name: "Build wheel" 121 | # Specifying the plat-name argument is necessary to build a wheel with the correct name, 122 | # see issue #350 for more information 123 | run: python3 setup.py bdist_wheel --plat-name macosx_11_0_x86_64 --verbose 124 | 125 | - uses: actions/upload-artifact@v4 126 | with: 127 | name: bdkpython-macos-x86_64-${{ matrix.python }} 128 | path: /Users/runner/work/bdk-ffi/bdk-ffi/bdk-python/dist/*.whl 129 | 130 | build-windows-wheels: 131 | name: "Build Windows wheel" 132 | runs-on: windows-2022 133 | defaults: 134 | run: 135 | working-directory: bdk-python 136 | strategy: 137 | matrix: 138 | python: 139 | - "3.8" 140 | - "3.9" 141 | - "3.10" 142 | - "3.11" 143 | - "3.12" 144 | steps: 145 | - name: "Checkout" 146 | uses: actions/checkout@v4 147 | with: 148 | submodules: true 149 | persist-credentials: false 150 | 151 | - uses: actions/setup-python@v4 152 | with: 153 | python-version: ${{ matrix.python }} 154 | 155 | - name: "Generate bdk.py and binaries" 156 | run: bash ./scripts/generate-windows.sh 157 | 158 | - name: "Build wheel" 159 | run: python setup.py bdist_wheel --verbose 160 | 161 | - name: "Upload artifacts" 162 | uses: actions/upload-artifact@v4 163 | with: 164 | name: bdkpython-win-${{ matrix.python }} 165 | path: D:\a\bdk-ffi\bdk-ffi\bdk-python\dist\*.whl 166 | 167 | publish-pypi: 168 | name: "Publish on PyPI" 169 | runs-on: ubuntu-24.04 170 | defaults: 171 | run: 172 | working-directory: bdk-python 173 | needs: [build-manylinux_2_28-x86_64-wheels, build-macos-arm64-wheels, build-macos-x86_64-wheels, build-windows-wheels] 174 | steps: 175 | - name: "Checkout" 176 | uses: actions/checkout@v4 177 | with: 178 | persist-credentials: false 179 | 180 | - name: "Download artifacts in dist/ directory" 181 | uses: actions/download-artifact@v4 182 | with: 183 | path: dist/ 184 | 185 | # - name: "Publish on test PyPI" 186 | # uses: pypa/gh-action-pypi-publish@release/v1 187 | # with: 188 | # user: __token__ 189 | # password: ${{ secrets.TEST_PYPI_API_TOKEN }} 190 | # repository_url: https://test.pypi.org/legacy/ 191 | # packages_dir: dist/*/ 192 | 193 | - name: "Publish on PyPI" 194 | uses: pypa/gh-action-pypi-publish@release/v1 195 | with: 196 | user: __token__ 197 | password: ${{ secrets.PYPI_API_TOKEN }} 198 | packages_dir: dist/*/ 199 | -------------------------------------------------------------------------------- /.github/workflows/test-android.yaml: -------------------------------------------------------------------------------- 1 | name: Test Android 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - "bdk-ffi/**" 7 | - "bdk-android/**" 8 | pull_request: 9 | paths: 10 | - "bdk-ffi/**" 11 | - "bdk-android/**" 12 | 13 | # The default Android NDK on the ubuntu-24.04 image is 25.2.9519653 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - name: "Show default version of NDK" 22 | run: echo $ANDROID_NDK_ROOT 23 | 24 | - name: "Check out PR branch" 25 | uses: actions/checkout@v4 26 | with: 27 | persist-credentials: false 28 | 29 | - name: "Cache" 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.cargo/registry 34 | ~/.cargo/git 35 | ./target 36 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 37 | 38 | - name: "Set up JDK" 39 | uses: actions/setup-java@v4 40 | with: 41 | distribution: temurin 42 | java-version: 17 43 | 44 | - name: "Build Android library" 45 | run: | 46 | cd bdk-android 47 | bash ./scripts/build-linux-x86_64.sh 48 | 49 | # There are currently no unit tests for bdk-android (see the tests in bdk-jvm instead) and the 50 | # integration tests require the macOS image which is not working with the older NDK version we 51 | # are using, so for now we just make sure that the library builds and omit the connectedTest 52 | # - name: "Run Android connected tests" 53 | # run: | 54 | # cd bdk-android 55 | # ./gradlew connectedAndroidTest --console=plain 56 | -------------------------------------------------------------------------------- /.github/workflows/test-jvm.yaml: -------------------------------------------------------------------------------- 1 | name: Test Kotlin/JVM 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - "bdk-ffi/**" 7 | - "bdk-jvm/**" 8 | pull_request: 9 | paths: 10 | - "bdk-ffi/**" 11 | - "bdk-jvm/**" 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - name: "Check out PR branch" 20 | uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | 24 | - name: "Cache" 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | ./target 31 | key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 32 | 33 | - name: "Set up JDK" 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: temurin 37 | java-version: 17 38 | 39 | - name: "Run JVM tests" 40 | run: | 41 | cd bdk-jvm 42 | bash ./scripts/build-linux-x86_64.sh 43 | ./gradlew test -P excludeConnectedTests 44 | -------------------------------------------------------------------------------- /.github/workflows/test-swift.yaml: -------------------------------------------------------------------------------- 1 | name: Test Swift 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - "bdk-ffi/**" 7 | - "bdk-swift/**" 8 | pull_request: 9 | paths: 10 | - "bdk-ffi/**" 11 | - "bdk-swift/**" 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | build: 17 | name: "Build and test" 18 | runs-on: macos-13 19 | steps: 20 | - name: "Checkout" 21 | uses: actions/checkout@v4 22 | with: 23 | persist-credentials: false 24 | 25 | - name: "Build Swift package" 26 | working-directory: bdk-swift 27 | run: bash ./build-xcframework.sh 28 | 29 | - name: "Run Swift tests" 30 | working-directory: bdk-swift 31 | run: swift test --filter Offline 32 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: Zizmor Actions Analysis 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | jobs: 10 | zizmor: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | security-events: write 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | persist-credentials: false 19 | 20 | - name: Rust Cache 21 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 22 | 23 | - name: Install zizmor 24 | run: cargo install zizmor --locked --version 1.6.0 25 | 26 | - name: Run zizmor 🌈 27 | run: zizmor --format sarif . > results.sarif 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Upload SARIF file 32 | uses: github/codeql-action/upload-sarif@v3 33 | with: 34 | sarif_file: results.sarif 35 | category: zizmor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust related 2 | target 3 | build 4 | 5 | # Kotlin related 6 | .gradle 7 | wallet_db 8 | bdk_ffi_test 9 | local.properties 10 | *.log 11 | *.dylib 12 | *.so 13 | *.db 14 | */data/signet 15 | .DS_Store 16 | testdb 17 | .lsp 18 | .clj-kondo 19 | .idea/ 20 | .editorconfig 21 | bdk.kt 22 | .kotlin/ 23 | 24 | # Swift related 25 | /.build 26 | .swiftpm 27 | /Packages 28 | /*.xcodeproj 29 | xcuserdata/ 30 | DerivedData/ 31 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 32 | bdkFFI.xcframework.zip 33 | bdkFFI 34 | libbdkffi.a 35 | bdkFFI.h 36 | BitcoinDevKitFFI.h 37 | BitcoinDevKit.swift 38 | bdk.swift 39 | .build 40 | *.xcframework/ 41 | Info.plist 42 | bdkffi.xcframework 43 | Sources/ 44 | xcuserdata 45 | 46 | # Python related 47 | __pycache__ 48 | .localenvironment/ 49 | -------------------------------------------------------------------------------- /DEVELOPMENT_CYCLE.md: -------------------------------------------------------------------------------- 1 | # Development Cycle 2 | 3 | This project follows a regular releasing schedule similar to the one [used by the Rust language] 4 | except releases always follow the latest [`bdk`] release by one to two weeks. In short, this means 5 | that a new release is made at a regular cadence, with all the feature/bugfixes that made it to 6 | `master` in time. This ensures that we don't keep delaying releases waiting for 7 | "just one more little thing". 8 | 9 | After making a new `bdk-ffi` release tag all downstream language bindings should also be updated. 10 | 11 | This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it 12 | is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be 13 | considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API 14 | changes in the changelog info attached to each release tag. 15 | 16 | We decided to maintain a faster release cycle while the library is still in "beta", i.e. before 17 | release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing 18 | issues, we want developers to have access to those updates as fast as possible. For this reason we 19 | will make a release **every 4 weeks**. 20 | 21 | Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer 22 | release cycles of **6 weeks**. 23 | 24 | The "feature freeze" will happen when [`bdk`] releases a release candidate. This project will then 25 | be updated and tested with [`bdk`] release candidates until a final release is published. This 26 | means a new branch will be created originating from the `master` tip at that time, and in that 27 | branch we will stop adding new features and only focus on ensuring the ones we've added are working 28 | properly. 29 | 30 | To create a new release a release manager will create a new issue using a `Release` template and 31 | follow the template instructions. 32 | 33 | [used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html 34 | [Semantic Versioning]: https://semver.org/ 35 | [`bdk`]: https://github.com/bitcoindevkit/bdk 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under [Apache 2.0](LICENSE-APACHE) or 2 | [MIT](LICENSE-MIT), at your option. 3 | 4 | Some files retain their own copyright notice, however, for full authorship 5 | information, see version control history. 6 | 7 | Except as otherwise noted in individual files, all files in this repository are 8 | licensed under the Apache License, Version 2.0 or the MIT license , at your option. 11 | 12 | You may not use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of this software or any files in this repository except in 14 | accordance with one or both of these licenses. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 5 | the Software, and to permit persons to whom the Software is furnished to do so, 6 | subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /PGP-BDK-BINDINGS.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mDMEYw6xkRYJKwYBBAHaRw8BAQdAg+VLXuidDqeP015H/QMlESJyQeIntTUoQkbk 4 | +IFu+jO0M2JpdGNvaW5kZXZraXQtYmluZGluZ3MgPGJpbmRpbmdzQGJpdGNvaW5k 5 | ZXZraXQub3JnPoiTBBMWCgA7FiEEiK2TrEWJ/QkP87jRJ2jEPogDxqMFAmMOsZEC 6 | GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQJ2jEPogDxqPQTgEA292D 7 | RQaxDTJ4k91D0w50Vrd0NSNUwlsERz9XJ64abWABAP99vGMmq2pfrngTQqjLgLe8 8 | 0YhQ+VML2x/B0LSN6MgNuDgEYw6xkRIKKwYBBAGXVQEFAQEHQEkUJv+/Wzx7nNiX 9 | eti3HkeT6ZNAuCExPE4F7jxHNQ1TAwEIB4h4BBgWCgAgFiEEiK2TrEWJ/QkP87jR 10 | J2jEPogDxqMFAmMOsZECGwwACgkQJ2jEPogDxqObPQEA/B0xNew03KM0JP630efG 11 | QT/3Caq/jx86pLwnB7XqWI8BAOKmqrOEiwCBjhaIpzC3/1M+aZuPRUL3V91uPxpM 12 | jFAJ 13 | =vvmK 14 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /bdk-android/README.md: -------------------------------------------------------------------------------- 1 | # bdk-android 2 | 3 | This project builds an .aar package for the Android platform that provide Kotlin language bindings for the [BDK] libraries. The Kotlin language bindings are created by the [`bdk-ffi`] project which is included in the root of this repository. 4 | 5 | ## How to Use 6 | 7 | To use the Kotlin language bindings for BDK in your Android project add the following to your gradle dependencies: 8 | 9 | ```kotlin 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation("org.bitcoindevkit:bdk-android:") 16 | } 17 | ``` 18 | 19 | ### Snapshot releases 20 | 21 | To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block: 22 | ```kotlin 23 | repositories { 24 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") 25 | } 26 | 27 | dependencies { 28 | implementation("org.bitcoindevkit:bdk-android:") 29 | } 30 | ``` 31 | 32 | ### Example Projects 33 | 34 | * [Devkit Wallet](https://github.com/bitcoindevkit/devkit-wallet) 35 | * [Padawan Wallet](https://github.com/thunderbiscuit/padawan-wallet) 36 | 37 | ### How to build 38 | 39 | _Note that Kotlin version `2.1.10` or later is required to build the library._ 40 | 41 | 1. Clone this repository. 42 | ```shell 43 | git clone https://github.com/bitcoindevkit/bdk-ffi 44 | ``` 45 | 2. Install Android SDK and Build-Tools for API level 30+ 46 | 3. Setup `ANDROID_SDK_ROOT` and `ANDROID_NDK_ROOT` path variables which are required by the build tool. Note that currently, NDK version 27.2.12479018 or above is recommended. For example: 47 | ```shell 48 | # macOS 49 | export ANDROID_SDK_ROOT=~/Library/Android/sdk 50 | export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018 51 | 52 | # Linux 53 | export ANDROID_SDK_ROOT=/usr/local/lib/android/sdk 54 | export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018 55 | ``` 56 | 4. Build Kotlin bindings 57 | ```sh 58 | # build Android library 59 | cd bdk-android 60 | bash ./scripts/build-.sh 61 | ``` 62 | 5. Start android emulator and run tests 63 | ```sh 64 | ./gradlew connectedAndroidTest 65 | ``` 66 | 67 | ## How to publish to your local Maven repo 68 | 69 | ```shell 70 | cd bdk-android 71 | ./gradlew publishToMavenLocal -P localBuild 72 | ``` 73 | 74 | Note that the command above assumes 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: 75 | ```properties 76 | signing.gnupg.keyName= 77 | signing.gnupg.passphrase= 78 | ``` 79 | 80 | and use the `publishToMavenLocal` task without the `localBuild` flag: 81 | ```shell 82 | ./gradlew publishToMavenLocal 83 | ``` 84 | 85 | ## Known issues 86 | 87 | ### JNA dependency 88 | 89 | Depending on the JVM version you use, you might not have the JNA dependency on your classpath. The exception thrown will be 90 | 91 | ```shell 92 | class file for com.sun.jna.Pointer not found 93 | ``` 94 | 95 | The solution is to add JNA as a dependency like so: 96 | 97 | ```kotlin 98 | dependencies { 99 | implementation("net.java.dev.jna:jna:5.12.1") 100 | } 101 | ``` 102 | 103 | ### x86 emulators 104 | 105 | For some older versions of macOS, Android Studio will recommend users install the x86 version of the emulator by default. This will not work with the bdk-android library, as we do not support 32-bit architectures. Make sure you install an x86_64 emulator to work with bdk-android. 106 | 107 | [BDK]: https://github.com/bitcoindevkit/ 108 | [`bdk-ffi`]: https://github.com/bitcoindevkit/bdk-ffi 109 | -------------------------------------------------------------------------------- /bdk-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library").version("8.3.1").apply(false) 3 | id("org.jetbrains.kotlin.android").version("2.1.10").apply(false) 4 | id("org.gradle.maven-publish") 5 | id("org.gradle.signing") 6 | id("io.github.gradle-nexus.publish-plugin").version("1.1.0").apply(true) 7 | id("org.jetbrains.dokka").version("2.0.0").apply(false) 8 | id("org.jetbrains.dokka-javadoc").version("2.0.0").apply(false) 9 | } 10 | 11 | // library version is defined in gradle.properties 12 | val libraryVersion: String by project 13 | 14 | // These properties are required here so that the nexus publish-plugin 15 | // finds a staging profile with the correct group (group is otherwise set as "") 16 | // and knows whether to publish to a SNAPSHOT repository or not 17 | // https://github.com/gradle-nexus/publish-plugin#applying-the-plugin 18 | group = "org.bitcoindevkit" 19 | version = libraryVersion 20 | 21 | nexusPublishing { 22 | repositories { 23 | create("sonatype") { 24 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 25 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 26 | 27 | val ossrhUsername: String? by project 28 | val ossrhPassword: String? by project 29 | username.set(ossrhUsername) 30 | password.set(ossrhPassword) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bdk-android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=official 5 | libraryVersion=2.0.0-SNAPSHOT 6 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 7 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 8 | -------------------------------------------------------------------------------- /bdk-android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /bdk-android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /bdk-android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /bdk-android/justfile: -------------------------------------------------------------------------------- 1 | [group("Repo")] 2 | [doc("Default command; list all available commands.")] 3 | @list: 4 | just --list --unsorted 5 | 6 | [group("Repo")] 7 | [doc("Open repo on GitHub in your default browser.")] 8 | repo: 9 | open https://github.com/bitcoindevkit/bdk-ffi 10 | 11 | [group("Repo")] 12 | [doc("Build the API docs.")] 13 | docs: 14 | ./gradlew :lib:dokkaGeneratePublicationHtml 15 | 16 | [group("Repo")] 17 | [doc("Publish the library to your local Maven repository.")] 18 | publish-local: 19 | ./gradlew publishToMavenLocal -P localBuild 20 | 21 | [group("Build")] 22 | [doc("Build the library for given ARCH.")] 23 | build ARCH="macos-aarch64": 24 | bash ./scripts/build-{{ARCH}}.sh 25 | 26 | [group("Build")] 27 | [doc("List available architectures for the build command.")] 28 | @list-architectures: 29 | echo "Available architectures:" 30 | echo " - linux-x86_64" 31 | echo " - macos-aarch64" 32 | echo " - windows-x86_64" 33 | 34 | [group("Build")] 35 | [doc("Remove all caches and previous build directories to start from scratch.")] 36 | clean: 37 | rm -rf ../bdk-ffi/target/ 38 | rm -rf ./build/ 39 | rm -rf ./lib/build/ 40 | rm -rf ./plugins/build/ 41 | 42 | [group("Test")] 43 | [doc("Run all tests.")] 44 | test: 45 | ./gradlew connectedAndroidTest 46 | -------------------------------------------------------------------------------- /bdk-android/lib/README.md: -------------------------------------------------------------------------------- 1 | # Module bdk-android 2 | 3 | The [bitcoindevkit](https://bitcoindevkit.org/) language bindings library for Kotlin on Android. 4 | 5 | # Package org.bitcoindevkit 6 | 7 | The functionality exposed in this package is in fact a combination of the [bdk_wallet](https://crates.io/crates/bdk_wallet), [bdk_core](https://crates.io/crates/bdk_core), [bdk_electrum](https://crates.io/crates/bdk_electrum), [bdk_esplora](https://crates.io/crates/bdk_esplora), [bitcoin](https://crates.io/crates/bitcoin), and [miniscript](https://crates.io/crates/miniscript) crates. 8 | -------------------------------------------------------------------------------- /bdk-android/lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | // library version is defined in gradle.properties 4 | val libraryVersion: String by project 5 | 6 | plugins { 7 | id("com.android.library") 8 | id("org.jetbrains.kotlin.android") 9 | id("org.gradle.maven-publish") 10 | id("org.gradle.signing") 11 | id("org.jetbrains.dokka") 12 | id("org.jetbrains.dokka-javadoc") 13 | } 14 | 15 | android { 16 | namespace = "org.bitcoindevkit" 17 | compileSdk = 34 18 | 19 | defaultConfig { 20 | minSdk = 24 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | consumerProguardFiles("consumer-rules.pro") 23 | } 24 | 25 | buildTypes { 26 | getByName("release") { 27 | isMinifyEnabled = false 28 | proguardFiles(file("proguard-android-optimize.txt"), file("proguard-rules.pro")) 29 | } 30 | } 31 | 32 | publishing { 33 | singleVariant("release") { 34 | withSourcesJar() 35 | withJavadocJar() 36 | } 37 | } 38 | } 39 | 40 | kotlin { 41 | tasks.withType().configureEach { 42 | kotlinOptions { 43 | jvmTarget = "17" 44 | } 45 | } 46 | } 47 | 48 | java { 49 | toolchain { 50 | languageVersion.set(JavaLanguageVersion.of(17)) 51 | } 52 | } 53 | 54 | dependencies { 55 | implementation("net.java.dev.jna:jna:5.14.0@aar") 56 | implementation("androidx.appcompat:appcompat:1.4.0") 57 | implementation("androidx.core:core-ktx:1.7.0") 58 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 59 | api("org.slf4j:slf4j-api:1.7.30") 60 | 61 | androidTestImplementation("com.github.tony19:logback-android:2.0.0") 62 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 63 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 64 | androidTestImplementation("org.jetbrains.kotlin:kotlin-test:1.6.10") 65 | androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.6.10") 66 | } 67 | 68 | afterEvaluate { 69 | publishing { 70 | publications { 71 | create("maven") { 72 | groupId = "org.bitcoindevkit" 73 | artifactId = "bdk-android" 74 | version = libraryVersion 75 | 76 | from(components["release"]) 77 | pom { 78 | name.set("bdk-android") 79 | description.set("Bitcoin Dev Kit Kotlin language bindings.") 80 | url.set("https://bitcoindevkit.org") 81 | licenses { 82 | license { 83 | name.set("APACHE 2.0") 84 | url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-APACHE") 85 | } 86 | license { 87 | name.set("MIT") 88 | url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") 89 | } 90 | } 91 | developers { 92 | developer { 93 | id.set("bdkdevelopers") 94 | name.set("Bitcoin Dev Kit Developers") 95 | email.set("dev@bitcoindevkit.org") 96 | } 97 | } 98 | scm { 99 | connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") 100 | developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") 101 | url.set("https://github.com/bitcoindevkit/bdk-ffi/tree/master") 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | signing { 110 | if (project.hasProperty("localBuild")) { 111 | isRequired = false 112 | } 113 | 114 | val signingKeyId: String? by project 115 | val signingKey: String? by project 116 | val signingPassword: String? by project 117 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 118 | sign(publishing.publications) 119 | } 120 | 121 | dokka { 122 | moduleName.set("bdk-android") 123 | moduleVersion.set(libraryVersion) 124 | dokkaSourceSets.main { 125 | includes.from("README.md") 126 | sourceLink { 127 | localDirectory.set(file("src/main/kotlin")) 128 | remoteUrl("https://bitcoindevkit.org/") 129 | remoteLineSuffix.set("#L") 130 | } 131 | } 132 | pluginsConfiguration.html { 133 | // customStyleSheets.from("styles.css") 134 | // customAssets.from("logo.svg") 135 | footerMessage.set("(c) Bitcoin Dev Kit Developers") 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /bdk-android/lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # for JNA 24 | -dontwarn java.awt.* 25 | -keep class com.sun.jna.* { *; } 26 | -keep class org.bitcoindevkit.* { *; } 27 | -keepclassmembers class * extends org.bitcoindevkit.* { public *; } 28 | -keepclassmembers class * extends com.sun.jna.* { public *; } 29 | -------------------------------------------------------------------------------- /bdk-android/lib/src/androidTest/assets/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %logger{12} 5 | 6 | 7 | [%-20thread] %msg 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import kotlin.test.AfterTest 8 | import kotlin.test.assertTrue 9 | import java.io.File 10 | 11 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 12 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 13 | 14 | @RunWith(AndroidJUnit4::class) 15 | class LiveTxBuilderTest { 16 | private val persistenceFilePath = InstrumentationRegistry.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence3.sqlite" 17 | private val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", Network.SIGNET) 18 | private val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", Network.SIGNET) 19 | 20 | @AfterTest 21 | fun cleanup() { 22 | val file = File(persistenceFilePath) 23 | if (file.exists()) { 24 | file.delete() 25 | } 26 | } 27 | 28 | @Test 29 | fun testTxBuilder() { 30 | var conn: Persister = Persister.newInMemory() 31 | val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 32 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 33 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 34 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 35 | wallet.applyUpdate(update) 36 | println("Balance: ${wallet.balance().total.toSat()}") 37 | 38 | assert(wallet.balance().total.toSat() > 0uL) { 39 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 40 | } 41 | 42 | val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 43 | val psbt: Psbt = TxBuilder() 44 | .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL)) 45 | .feeRate(FeeRate.fromSatPerVb(2uL)) 46 | .finish(wallet) 47 | 48 | println(psbt.serialize()) 49 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 50 | } 51 | 52 | @Test 53 | fun complexTxBuilder() { 54 | var conn: Persister = Persister.newInMemory() 55 | val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 56 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 57 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 58 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 59 | wallet.applyUpdate(update) 60 | 61 | println("Balance: ${wallet.balance().total.toSat()}") 62 | 63 | assert(wallet.balance().total.toSat() > 0uL) { 64 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 65 | } 66 | 67 | val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 68 | val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET) 69 | val allRecipients: List = listOf( 70 | ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)), 71 | ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)), 72 | ) 73 | 74 | val psbt: Psbt = TxBuilder() 75 | .setRecipients(allRecipients) 76 | .feeRate(FeeRate.fromSatPerVb(4uL)) 77 | .finish(wallet) 78 | 79 | wallet.sign(psbt) 80 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import org.junit.Test 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.platform.app.InstrumentationRegistry 6 | import org.junit.runner.RunWith 7 | import kotlin.test.AfterTest 8 | import kotlin.test.assertTrue 9 | import java.io.File 10 | 11 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 12 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 13 | 14 | @RunWith(AndroidJUnit4::class) 15 | class LiveWalletTest { 16 | private val persistenceFilePath = InstrumentationRegistry 17 | .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence2.sqlite" 18 | private val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", Network.SIGNET) 19 | private val changeDescriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", Network.SIGNET) 20 | 21 | @AfterTest 22 | fun cleanup() { 23 | val file = File(persistenceFilePath) 24 | if (file.exists()) { 25 | file.delete() 26 | } 27 | } 28 | 29 | @Test 30 | fun testSyncedBalance() { 31 | var conn: Persister = Persister.newInMemory() 32 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 33 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 34 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 35 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 36 | wallet.applyUpdate(update) 37 | println("Balance: ${wallet.balance().total.toSat()}") 38 | val balance: Balance = wallet.balance() 39 | println("Balance: $balance") 40 | 41 | assert(wallet.balance().total.toSat() > 0uL) { 42 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 43 | } 44 | 45 | println("Transactions count: ${wallet.transactions().count()}") 46 | val transactions = wallet.transactions().take(3) 47 | for (tx in transactions) { 48 | val sentAndReceived = wallet.sentAndReceived(tx.transaction) 49 | println("Transaction: ${tx.transaction.computeTxid()}") 50 | println("Sent ${sentAndReceived.sent.toSat()}") 51 | println("Received ${sentAndReceived.received.toSat()}") 52 | } 53 | } 54 | 55 | @Test 56 | fun testBroadcastTransaction() { 57 | var conn: Persister = Persister.newInMemory() 58 | val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 59 | val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) 60 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 61 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 62 | wallet.applyUpdate(update) 63 | println("Balance: ${wallet.balance().total.toSat()}") 64 | 65 | assert(wallet.balance().total.toSat() > 0uL) { 66 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 67 | } 68 | 69 | val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 70 | 71 | val psbt: Psbt = TxBuilder() 72 | .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL)) 73 | .feeRate(FeeRate.fromSatPerVb(4uL)) 74 | .finish(wallet) 75 | 76 | println(psbt.serialize()) 77 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 78 | 79 | val walletDidSign = wallet.sign(psbt) 80 | assertTrue(walletDidSign) 81 | 82 | val tx: Transaction = psbt.extractTx() 83 | println("Txid is: ${tx.computeTxid()}") 84 | 85 | val txFee: Amount = wallet.calculateFee(tx) 86 | println("Tx fee is: ${txFee.toSat()}") 87 | 88 | val feeRate: FeeRate = wallet.calculateFeeRate(tx) 89 | println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB") 90 | 91 | esploraClient.broadcast(tx) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineDescriptorTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import org.junit.runner.RunWith 7 | 8 | @RunWith(AndroidJUnit4::class) 9 | class OfflineDescriptorTest { 10 | @Test 11 | fun testDescriptorBip86() { 12 | val mnemonic: Mnemonic = Mnemonic.fromString("space echo position wrist orient erupt relief museum myself grain wisdom tumble") 13 | val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) 14 | val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) 15 | 16 | assertEquals( 17 | expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", 18 | actual = descriptor.toString() 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertTrue 6 | import kotlin.test.assertFalse 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import androidx.test.platform.app.InstrumentationRegistry 9 | import org.junit.runner.RunWith 10 | import kotlin.test.AfterTest 11 | import java.io.File 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | class OfflineWalletTest { 15 | private val persistenceFilePath = InstrumentationRegistry 16 | .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence1.sqlite" 17 | private val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", Network.TESTNET) 18 | private val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", Network.TESTNET) 19 | 20 | @AfterTest 21 | fun cleanup() { 22 | val file = File(persistenceFilePath) 23 | if (file.exists()) { 24 | file.delete() 25 | } 26 | } 27 | 28 | @Test 29 | fun testDescriptorBip86() { 30 | val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12) 31 | val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) 32 | val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) 33 | 34 | assertTrue(descriptor.toString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'") 35 | } 36 | 37 | @Test 38 | fun testNewAddress() { 39 | val conn = Persister.newInMemory() 40 | val wallet: Wallet = Wallet( 41 | descriptor, 42 | changeDescriptor, 43 | Network.TESTNET, 44 | conn 45 | ) 46 | val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) 47 | 48 | assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") 49 | assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") 50 | assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") 51 | assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") 52 | 53 | assertEquals( 54 | expected = "tb1qhjys9wxlfykmte7ftryptx975uqgd6kcm6a7z4", 55 | actual = addressInfo.address.toString() 56 | ) 57 | } 58 | 59 | @Test 60 | fun testBalance() { 61 | var conn: Persister = Persister.newInMemory() 62 | val wallet: Wallet = Wallet( 63 | descriptor, 64 | changeDescriptor, 65 | Network.TESTNET, 66 | conn 67 | ) 68 | 69 | assertEquals( 70 | expected = 0uL, 71 | actual = wallet.balance().total.toSat() 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bdk-android/lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /bdk-android/scripts/build-linux-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$ANDROID_NDK_ROOT" ]; then 4 | echo "Error: ANDROID_NDK_ROOT is not defined in your environment" 5 | exit 1 6 | fi 7 | 8 | PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" 9 | CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=24" 10 | AR="llvm-ar" 11 | LIB_NAME="libbdkffi.so" 12 | COMPILATION_TARGET_ARM64_V8A="aarch64-linux-android" 13 | COMPILATION_TARGET_X86_64="x86_64-linux-android" 14 | COMPILATION_TARGET_ARMEABI_V7A="armv7-linux-androideabi" 15 | RESOURCE_DIR_ARM64_V8A="arm64-v8a" 16 | RESOURCE_DIR_X86_64="x86_64" 17 | RESOURCE_DIR_ARMEABI_V7A="armeabi-v7a" 18 | 19 | # Move to the Rust library directory 20 | cd ../bdk-ffi/ || exit 21 | rustup default 1.84.1 22 | rustup target add $COMPILATION_TARGET_ARM64_V8A $COMPILATION_TARGET_ARMEABI_V7A $COMPILATION_TARGET_X86_64 23 | 24 | # Build the binaries 25 | # The CC and CARGO_TARGET__LINUX_ANDROID_LINKER environment variables must be declared on the same line as the cargo build command 26 | CC="aarch64-linux-android24-clang" CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARM64_V8A 27 | CC="x86_64-linux-android24-clang" CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_X86_64 28 | CC="armv7a-linux-androideabi24-clang" CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARMEABI_V7A 29 | 30 | # Copy the binaries to their respective resource directories 31 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 32 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 33 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 34 | cp ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 35 | cp ./target/$COMPILATION_TARGET_ARMEABI_V7A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 36 | cp ./target/$COMPILATION_TARGET_X86_64/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 37 | 38 | # Generate Kotlin bindings using uniffi-bindgen 39 | cargo run --bin uniffi-bindgen generate --library ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME --language kotlin --out-dir ../bdk-android/lib/src/main/kotlin/ --no-format 40 | -------------------------------------------------------------------------------- /bdk-android/scripts/build-macos-aarch64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$ANDROID_NDK_ROOT" ]; then 4 | echo "Error: ANDROID_NDK_ROOT is not defined in your environment" 5 | exit 1 6 | fi 7 | 8 | PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH" 9 | CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=24" 10 | AR="llvm-ar" 11 | LIB_NAME="libbdkffi.so" 12 | COMPILATION_TARGET_ARM64_V8A="aarch64-linux-android" 13 | COMPILATION_TARGET_X86_64="x86_64-linux-android" 14 | COMPILATION_TARGET_ARMEABI_V7A="armv7-linux-androideabi" 15 | RESOURCE_DIR_ARM64_V8A="arm64-v8a" 16 | RESOURCE_DIR_X86_64="x86_64" 17 | RESOURCE_DIR_ARMEABI_V7A="armeabi-v7a" 18 | 19 | # Move to the Rust library directory 20 | cd ../bdk-ffi/ || exit 21 | rustup default 1.84.1 22 | rustup target add $COMPILATION_TARGET_ARM64_V8A $COMPILATION_TARGET_ARMEABI_V7A $COMPILATION_TARGET_X86_64 23 | 24 | # Build the binaries 25 | # The CC and CARGO_TARGET__LINUX_ANDROID_LINKER environment variables must be declared on the same line as the cargo build command 26 | CC="aarch64-linux-android24-clang" CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARM64_V8A 27 | CC="x86_64-linux-android24-clang" CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_X86_64 28 | CC="armv7a-linux-androideabi24-clang" CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi24-clang" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARMEABI_V7A 29 | 30 | # Copy the binaries to their respective resource directories 31 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 32 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 33 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 34 | cp ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 35 | cp ./target/$COMPILATION_TARGET_ARMEABI_V7A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 36 | cp ./target/$COMPILATION_TARGET_X86_64/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 37 | 38 | # Generate Kotlin bindings using uniffi-bindgen 39 | cargo run --bin uniffi-bindgen generate --library ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME --language kotlin --out-dir ../bdk-android/lib/src/main/kotlin/ --no-format 40 | -------------------------------------------------------------------------------- /bdk-android/scripts/build-windows-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$ANDROID_NDK_ROOT" ]; then 4 | echo "Error: ANDROID_NDK_ROOT is not defined in your environment" 5 | exit 1 6 | fi 7 | 8 | # Update PATH for Windows 9 | PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/windows-x86_64/bin:$PATH" 10 | CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=24" 11 | AR="llvm-ar" 12 | LIB_NAME="libbdkffi.so" 13 | COMPILATION_TARGET_ARM64_V8A="aarch64-linux-android" 14 | COMPILATION_TARGET_X86_64="x86_64-linux-android" 15 | COMPILATION_TARGET_ARMEABI_V7A="armv7-linux-androideabi" 16 | RESOURCE_DIR_ARM64_V8A="arm64-v8a" 17 | RESOURCE_DIR_X86_64="x86_64" 18 | RESOURCE_DIR_ARMEABI_V7A="armeabi-v7a" 19 | 20 | # Move to the Rust library directory 21 | cd ../bdk-ffi/ || exit 22 | rustup default 1.84.1 23 | rustup target add $COMPILATION_TARGET_ARM64_V8A $COMPILATION_TARGET_ARMEABI_V7A $COMPILATION_TARGET_X86_64 24 | 25 | # Build the binaries 26 | # The CC and CARGO_TARGET__LINUX_ANDROID_LINKER environment variables must be declared on the same line as the cargo build command 27 | CC="aarch64-linux-android24-clang.cmd" CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android24-clang.cmd" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARM64_V8A 28 | CC="x86_64-linux-android24-clang.cmd" CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android24-clang.cmd" cargo build --profile release-smaller --target $COMPILATION_TARGET_X86_64 29 | CC="armv7a-linux-androideabi24-clang.cmd" CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi24-clang.cmd" cargo build --profile release-smaller --target $COMPILATION_TARGET_ARMEABI_V7A 30 | 31 | # Copy the binaries to their respective resource directories 32 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 33 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 34 | mkdir -p ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 35 | cp ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARM64_V8A/ 36 | cp ./target/$COMPILATION_TARGET_ARMEABI_V7A/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_ARMEABI_V7A/ 37 | cp ./target/$COMPILATION_TARGET_X86_64/release-smaller/$LIB_NAME ../bdk-android/lib/src/main/jniLibs/$RESOURCE_DIR_X86_64/ 38 | 39 | # Generate Kotlin bindings using uniffi-bindgen. (Any of the other $COMPILATION_TARGET_* could have been used) 40 | cargo run --bin uniffi-bindgen generate --library ./target/$COMPILATION_TARGET_ARM64_V8A/release-smaller/$LIB_NAME --language kotlin --out-dir ../bdk-android/lib/src/main/kotlin/ --no-format 41 | -------------------------------------------------------------------------------- /bdk-android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bdk-android" 2 | 3 | include(":lib") 4 | 5 | pluginManagement { 6 | repositories { 7 | gradlePluginPortal() 8 | google() 9 | } 10 | } 11 | 12 | dependencyResolutionManagement { 13 | repositories { 14 | mavenCentral() 15 | google() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bdk-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bdk-ffi" 3 | version = "2.0.0-alpha.0" 4 | homepage = "https://bitcoindevkit.org" 5 | repository = "https://github.com/bitcoindevkit/bdk" 6 | edition = "2018" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [lib] 10 | crate-type = ["lib", "staticlib", "cdylib"] 11 | name = "bdkffi" 12 | 13 | [[bin]] 14 | name = "uniffi-bindgen" 15 | path = "uniffi-bindgen.rs" 16 | 17 | [features] 18 | default = ["uniffi/cli"] 19 | 20 | [dependencies] 21 | bdk_wallet = { version = "2.0.0", features = ["all-keys", "keys-bip39", "rusqlite"] } 22 | bdk_esplora = { version = "0.22.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] } 23 | bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls-ring"] } 24 | bdk_kyoto = { version = "0.11.0" } 25 | 26 | uniffi = { version = "=0.29.1" } 27 | thiserror = "1.0.58" 28 | 29 | [build-dependencies] 30 | uniffi = { version = "=0.29.1", features = ["build"] } 31 | 32 | [dev-dependencies] 33 | uniffi = { version = "=0.29.1", features = ["bindgen-tests"] } 34 | assert_matches = "1.5.0" 35 | 36 | [profile.release-smaller] 37 | inherits = "release" 38 | opt-level = 'z' # Optimize for size. 39 | lto = true # Enable Link Time Optimization 40 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 41 | panic = "abort" # Abort on panic 42 | strip = "debuginfo" # Partially strip symbols from binary 43 | -------------------------------------------------------------------------------- /bdk-ffi/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi::generate_scaffolding("./src/bdk.udl").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /bdk-ffi/justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --list 3 | 4 | build: 5 | cargo build --profile release-smaller 6 | 7 | check: 8 | cargo fmt 9 | cargo clippy 10 | 11 | test: 12 | cargo test --lib 13 | -------------------------------------------------------------------------------- /bdk-ffi/src/esplora.rs: -------------------------------------------------------------------------------- 1 | use crate::bitcoin::BlockHash; 2 | use crate::bitcoin::Transaction; 3 | use crate::bitcoin::Txid; 4 | use crate::error::EsploraError; 5 | use crate::types::Tx; 6 | use crate::types::TxStatus; 7 | use crate::types::Update; 8 | use crate::types::{FullScanRequest, SyncRequest}; 9 | 10 | use bdk_esplora::esplora_client::{BlockingClient, Builder}; 11 | use bdk_esplora::EsploraExt; 12 | use bdk_wallet::bitcoin::Transaction as BdkTransaction; 13 | use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest; 14 | use bdk_wallet::chain::spk_client::FullScanResponse as BdkFullScanResponse; 15 | use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest; 16 | use bdk_wallet::chain::spk_client::SyncResponse as BdkSyncResponse; 17 | use bdk_wallet::KeychainKind; 18 | use bdk_wallet::Update as BdkUpdate; 19 | 20 | use std::collections::{BTreeMap, HashMap}; 21 | use std::sync::Arc; 22 | 23 | /// Wrapper around an esplora_client::BlockingClient which includes an internal in-memory transaction 24 | /// cache to avoid re-fetching already downloaded transactions. 25 | #[derive(uniffi::Object)] 26 | pub struct EsploraClient(BlockingClient); 27 | 28 | #[uniffi::export] 29 | impl EsploraClient { 30 | /// Creates a new bdk client from an esplora_client::BlockingClient. 31 | /// Optional: Set the proxy of the builder. 32 | #[uniffi::constructor(default(proxy = None))] 33 | pub fn new(url: String, proxy: Option) -> Self { 34 | let mut builder = Builder::new(url.as_str()); 35 | if let Some(proxy) = proxy { 36 | builder = builder.proxy(proxy.as_str()); 37 | } 38 | Self(builder.build_blocking()) 39 | } 40 | 41 | /// Scan keychain scripts for transactions against Esplora, returning an update that can be 42 | /// applied to the receiving structures. 43 | /// 44 | /// `request` provides the data required to perform a script-pubkey-based full scan 45 | /// (see [`FullScanRequest`]). The full scan for each keychain (`K`) stops after a gap of 46 | /// `stop_gap` script pubkeys with no associated transactions. `parallel_requests` specifies 47 | /// the maximum number of HTTP requests to make in parallel. 48 | pub fn full_scan( 49 | &self, 50 | request: Arc, 51 | stop_gap: u64, 52 | parallel_requests: u64, 53 | ) -> Result, EsploraError> { 54 | // using option and take is not ideal but the only way to take full ownership of the request 55 | let request: BdkFullScanRequest = request 56 | .0 57 | .lock() 58 | .unwrap() 59 | .take() 60 | .ok_or(EsploraError::RequestAlreadyConsumed)?; 61 | 62 | let result: BdkFullScanResponse = 63 | self.0 64 | .full_scan(request, stop_gap as usize, parallel_requests as usize)?; 65 | 66 | let update = BdkUpdate { 67 | last_active_indices: result.last_active_indices, 68 | tx_update: result.tx_update, 69 | chain: result.chain_update, 70 | }; 71 | 72 | Ok(Arc::new(Update(update))) 73 | } 74 | 75 | /// Sync a set of scripts, txids, and/or outpoints against Esplora. 76 | /// 77 | /// `request` provides the data required to perform a script-pubkey-based sync (see 78 | /// [`SyncRequest`]). `parallel_requests` specifies the maximum number of HTTP requests to make 79 | /// in parallel. 80 | pub fn sync( 81 | &self, 82 | request: Arc, 83 | parallel_requests: u64, 84 | ) -> Result, EsploraError> { 85 | // using option and take is not ideal but the only way to take full ownership of the request 86 | let request: BdkSyncRequest<(KeychainKind, u32)> = request 87 | .0 88 | .lock() 89 | .unwrap() 90 | .take() 91 | .ok_or(EsploraError::RequestAlreadyConsumed)?; 92 | 93 | let result: BdkSyncResponse = self.0.sync(request, parallel_requests as usize)?; 94 | 95 | let update = BdkUpdate { 96 | last_active_indices: BTreeMap::default(), 97 | tx_update: result.tx_update, 98 | chain: result.chain_update, 99 | }; 100 | 101 | Ok(Arc::new(Update(update))) 102 | } 103 | 104 | /// Broadcast a [`Transaction`] to Esplora. 105 | pub fn broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> { 106 | let bdk_transaction: BdkTransaction = transaction.into(); 107 | self.0 108 | .broadcast(&bdk_transaction) 109 | .map_err(EsploraError::from) 110 | } 111 | 112 | /// Get a [`Transaction`] option given its [`Txid`]. 113 | pub fn get_tx(&self, txid: Arc) -> Result>, EsploraError> { 114 | let tx_opt = self.0.get_tx(&txid.0)?; 115 | Ok(tx_opt.map(|inner| Arc::new(Transaction::from(inner)))) 116 | } 117 | 118 | /// Get the height of the current blockchain tip. 119 | pub fn get_height(&self) -> Result { 120 | self.0.get_height().map_err(EsploraError::from) 121 | } 122 | 123 | /// Get a map where the key is the confirmation target (in number of 124 | /// blocks) and the value is the estimated feerate (in sat/vB). 125 | pub fn get_fee_estimates(&self) -> Result, EsploraError> { 126 | self.0.get_fee_estimates().map_err(EsploraError::from) 127 | } 128 | 129 | /// Get the [`BlockHash`] of a specific block height. 130 | pub fn get_block_hash(&self, block_height: u32) -> Result, EsploraError> { 131 | self.0 132 | .get_block_hash(block_height) 133 | .map(|hash| Arc::new(BlockHash(hash))) 134 | .map_err(EsploraError::from) 135 | } 136 | 137 | /// Get the status of a [`Transaction`] given its [`Txid`]. 138 | pub fn get_tx_status(&self, txid: Arc) -> Result { 139 | self.0 140 | .get_tx_status(&txid.0) 141 | .map(TxStatus::from) 142 | .map_err(EsploraError::from) 143 | } 144 | 145 | /// Get transaction info given its [`Txid`]. 146 | pub fn get_tx_info(&self, txid: Arc) -> Result, EsploraError> { 147 | self.0 148 | .get_tx_info(&txid.0) 149 | .map(|tx| tx.map(Tx::from)) 150 | .map_err(EsploraError::from) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /bdk-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod bitcoin; 2 | mod descriptor; 3 | mod electrum; 4 | mod error; 5 | mod esplora; 6 | mod keys; 7 | mod kyoto; 8 | mod macros; 9 | mod store; 10 | mod tx_builder; 11 | mod types; 12 | mod wallet; 13 | 14 | use crate::bitcoin::FeeRate; 15 | use crate::bitcoin::OutPoint; 16 | use crate::bitcoin::Script; 17 | use crate::error::AddressParseError; 18 | use crate::error::Bip32Error; 19 | use crate::error::Bip39Error; 20 | use crate::error::CreateTxError; 21 | use crate::error::DescriptorError; 22 | use crate::error::DescriptorKeyError; 23 | use crate::error::ElectrumError; 24 | use crate::error::EsploraError; 25 | use crate::error::FeeRateError; 26 | use crate::error::FromScriptError; 27 | use crate::error::MiniscriptError; 28 | use crate::error::ParseAmountError; 29 | use crate::error::PersistenceError; 30 | use crate::error::PsbtError; 31 | use crate::error::PsbtFinalizeError; 32 | use crate::error::PsbtParseError; 33 | use crate::error::RequestBuilderError; 34 | use crate::error::TransactionError; 35 | use crate::types::FullScanRequest; 36 | use crate::types::FullScanRequestBuilder; 37 | use crate::types::FullScanScriptInspector; 38 | use crate::types::LockTime; 39 | use crate::types::PkOrF; 40 | use crate::types::SyncRequest; 41 | use crate::types::SyncRequestBuilder; 42 | use crate::types::SyncScriptInspector; 43 | 44 | use bdk_wallet::bitcoin::Network; 45 | use bdk_wallet::keys::bip39::WordCount; 46 | use bdk_wallet::KeychainKind; 47 | 48 | uniffi::include_scaffolding!("bdk"); 49 | -------------------------------------------------------------------------------- /bdk-ffi/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! impl_from_core_type { 3 | ($core_type:ident, $ffi_type:ident) => { 4 | impl From<$core_type> for $ffi_type { 5 | fn from(core_type: $core_type) -> Self { 6 | $ffi_type(core_type) 7 | } 8 | } 9 | }; 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! impl_into_core_type { 14 | ($ffi_type:ident, $core_type:ident) => { 15 | impl From<$ffi_type> for $core_type { 16 | fn from(ffi_type: $ffi_type) -> Self { 17 | ffi_type.0 18 | } 19 | } 20 | }; 21 | } 22 | 23 | #[macro_export] 24 | macro_rules! impl_hash_like { 25 | ($ffi_type:ident, $core_type:ident) => { 26 | #[uniffi::export] 27 | impl $ffi_type { 28 | /// Construct a hash-like type from 32 bytes. 29 | #[uniffi::constructor] 30 | pub fn from_bytes(bytes: Vec) -> Result { 31 | let hash_like: $core_type = deserialize(&bytes).map_err(|_| { 32 | let len = bytes.len() as u32; 33 | HashParseError::InvalidHash { len } 34 | })?; 35 | Ok(Self(hash_like)) 36 | } 37 | 38 | /// Serialize this type into a 32 byte array. 39 | pub fn serialize(&self) -> Vec { 40 | serialize(&self.0) 41 | } 42 | } 43 | 44 | impl std::fmt::Display for $ffi_type { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | self.0.fmt(f) 47 | } 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /bdk-ffi/src/store.rs: -------------------------------------------------------------------------------- 1 | use crate::error::PersistenceError; 2 | use crate::types::ChangeSet; 3 | 4 | use bdk_wallet::{rusqlite::Connection as BdkConnection, WalletPersister}; 5 | 6 | use std::ops::DerefMut; 7 | use std::sync::{Arc, Mutex}; 8 | 9 | /// Definition of a wallet persistence implementation. 10 | #[uniffi::export(with_foreign)] 11 | pub trait Persistence: Send + Sync { 12 | /// Initialize the total aggregate `ChangeSet` for the underlying wallet. 13 | fn initialize(&self) -> Result, PersistenceError>; 14 | 15 | /// Persist a `ChangeSet` to the total aggregate changeset of the wallet. 16 | fn persist(&self, changeset: Arc) -> Result<(), PersistenceError>; 17 | } 18 | 19 | pub(crate) enum PersistenceType { 20 | Custom(Arc), 21 | Sql(Mutex), 22 | } 23 | 24 | /// Wallet backend implementations. 25 | #[derive(uniffi::Object)] 26 | pub struct Persister { 27 | pub(crate) inner: Mutex, 28 | } 29 | 30 | #[uniffi::export] 31 | impl Persister { 32 | /// Create a new Sqlite connection at the specified file path. 33 | #[uniffi::constructor] 34 | pub fn new_sqlite(path: String) -> Result { 35 | let conn = BdkConnection::open(path)?; 36 | Ok(Self { 37 | inner: PersistenceType::Sql(conn.into()).into(), 38 | }) 39 | } 40 | 41 | /// Create a new connection in memory. 42 | #[uniffi::constructor] 43 | pub fn new_in_memory() -> Result { 44 | let conn = BdkConnection::open_in_memory()?; 45 | Ok(Self { 46 | inner: PersistenceType::Sql(conn.into()).into(), 47 | }) 48 | } 49 | 50 | /// Use a native persistence layer. 51 | #[uniffi::constructor] 52 | pub fn custom(persistence: Arc) -> Self { 53 | Self { 54 | inner: PersistenceType::Custom(persistence).into(), 55 | } 56 | } 57 | } 58 | 59 | impl WalletPersister for PersistenceType { 60 | type Error = PersistenceError; 61 | 62 | fn initialize(persister: &mut Self) -> Result { 63 | match persister { 64 | PersistenceType::Sql(ref conn) => { 65 | let mut lock = conn.lock().unwrap(); 66 | let deref = lock.deref_mut(); 67 | Ok(BdkConnection::initialize(deref)?) 68 | } 69 | PersistenceType::Custom(any) => any 70 | .initialize() 71 | .map(|changeset| changeset.as_ref().clone().into()), 72 | } 73 | } 74 | 75 | fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> { 76 | match persister { 77 | PersistenceType::Sql(ref conn) => { 78 | let mut lock = conn.lock().unwrap(); 79 | let deref = lock.deref_mut(); 80 | Ok(BdkConnection::persist(deref, changeset)?) 81 | } 82 | PersistenceType::Custom(any) => { 83 | let ffi_changeset: ChangeSet = changeset.clone().into(); 84 | any.persist(Arc::new(ffi_changeset)) 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bdk-ffi/tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests for bdk-ffi 2 | 3 | This contains simple tests to make sure bdk-ffi can be used as a dependency for each of the 4 | supported bindings languages. 5 | 6 | To skip integration tests and only run unit tests use `cargo test --lib`. 7 | 8 | To run all tests including integration tests use `CLASSPATH=./tests/jna/jna-5.14.0.jar cargo test`. 9 | 10 | Before running integration tests you must install the following development tools: 11 | 12 | 1. [Java](https://openjdk.org/) and [Kotlin](https://kotlinlang.org/), 13 | [sdkman](https://sdkman.io/) can help: 14 | ```shell 15 | sdk install java 11.0.16.1-zulu 16 | sdk install kotlin 1.7.20` 17 | ``` 18 | 2. [Swift](https://www.swift.org/) 19 | 3. [Python](https://www.python.org/) 20 | -------------------------------------------------------------------------------- /bdk-ffi/tests/bindings/test.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a basic test kotlin program that does nothing but confirm that the kotlin bindings compile 3 | * and that a program that depends on them will run. 4 | */ 5 | 6 | import org.bitcoindevkit.bitcoin.Network 7 | 8 | // A type from bitcoin-ffi 9 | val network = Network.TESTNET 10 | -------------------------------------------------------------------------------- /bdk-ffi/tests/bindings/test.py: -------------------------------------------------------------------------------- 1 | from bdkpython.bitcoin import Network 2 | 3 | import unittest 4 | 5 | class TestBdk(unittest.TestCase): 6 | 7 | # A type from the bitcoin-ffi library 8 | def test_some_enum(self): 9 | network = Network.TESTNET 10 | 11 | if __name__=='__main__': 12 | unittest.main() 13 | -------------------------------------------------------------------------------- /bdk-ffi/tests/bindings/test.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a basic test swift program that does nothing but confirm that the swift bindings compile 3 | * and that a program that depends on them will run. 4 | */ 5 | 6 | import Foundation 7 | import BitcoinDevKit 8 | 9 | // A type from the bitcoin-ffi library 10 | let network = Network.testnet 11 | -------------------------------------------------------------------------------- /bdk-ffi/tests/jna/jna-5.14.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-ffi/tests/jna/jna-5.14.0.jar -------------------------------------------------------------------------------- /bdk-ffi/tests/test_generated_bindings.rs: -------------------------------------------------------------------------------- 1 | uniffi::build_foreign_language_testcases!( 2 | // Not sure why the new types break this Kotlin test and not the others, but the libraries work 3 | // fine. Commenting out for now. 4 | // "tests/bindings/test.kts", 5 | "tests/bindings/test.swift", 6 | // Weirdly enough, the Python tests below pass locally, but fail on the CI with the error: 7 | // ModuleNotFoundError: No module named 'bdkpython' 8 | // "tests/bindings/test.py", 9 | ); 10 | -------------------------------------------------------------------------------- /bdk-ffi/uniffi-android.toml: -------------------------------------------------------------------------------- 1 | [bindings.kotlin] 2 | android = true 3 | android_cleaner = true -------------------------------------------------------------------------------- /bdk-ffi/uniffi-bindgen.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi::uniffi_bindgen_main() 3 | } 4 | -------------------------------------------------------------------------------- /bdk-ffi/uniffi.toml: -------------------------------------------------------------------------------- 1 | [bindings.kotlin] 2 | package_name = "org.bitcoindevkit" 3 | cdylib_name = "bdkffi" 4 | 5 | [bindings.python] 6 | cdylib_name = "bdkffi" 7 | 8 | [bindings.swift] 9 | module_name = "BitcoinDevKit" 10 | cdylib_name = "bdkffi" 11 | -------------------------------------------------------------------------------- /bdk-jvm/README.md: -------------------------------------------------------------------------------- 1 | # bdk-jvm 2 | 3 | This project builds a .jar package for the JVM platform that provides Kotlin language bindings for the [BDK] libraries. The Kotlin language bindings are created by the `bdk-ffi` project which is included in the root of this repository. 4 | 5 | ## How to Use 6 | 7 | To use the Kotlin language bindings for BDK in your JVM project add the following to your gradle dependencies: 8 | 9 | ```kotlin 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation("org.bitcoindevkit:bdk-jvm:") 16 | } 17 | ``` 18 | 19 | ### Snapshot releases 20 | 21 | To use a snapshot release, specify the snapshot repository url in the `repositories` block and use the snapshot version in the `dependencies` block: 22 | 23 | ```kotlin 24 | repositories { 25 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") 26 | } 27 | 28 | dependencies { 29 | implementation("org.bitcoindevkit:bdk-jvm:") 30 | } 31 | ``` 32 | 33 | ## Example Projects 34 | 35 | - [Tatooine Faucet](https://github.com/thunderbiscuit/tatooine) 36 | - [Godzilla Wallet](https://github.com/thunderbiscuit/godzilla-wallet) 37 | 38 | ## How to build 39 | 40 | _Note that Kotlin version `1.9.23` or later is required to build the library._ 41 | 1. Install JDK 17. For example, with SDKMAN!: 42 | ```shell 43 | curl -s "https://get.sdkman.io" | bash 44 | source "$HOME/.sdkman/bin/sdkman-init.sh" 45 | sdk install java 17.0.2-tem 46 | ``` 47 | 2. Build kotlin bindings 48 | ```sh 49 | bash ./scripts/build-.sh 50 | ``` 51 | 52 | ## How to publish to your local Maven repo 53 | 54 | ```shell 55 | cd bdk-jvm 56 | ./gradlew publishToMavenLocal -P localBuild 57 | ``` 58 | 59 | 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: 60 | 61 | ```properties 62 | signing.gnupg.keyName= 63 | signing.gnupg.passphrase= 64 | ``` 65 | 66 | and use the `publishToMavenLocal` task without the `localBuild` flag: 67 | 68 | ```shell 69 | ./gradlew publishToMavenLocal 70 | ``` 71 | 72 | ## Known issues 73 | 74 | ## JNA dependency 75 | 76 | Depending on the JVM version you use, you might not have the JNA dependency on your classpath. The exception thrown will be 77 | ```shell 78 | class file for com.sun.jna.Pointer not found 79 | ``` 80 | 81 | The solution is to add JNA as a dependency like so: 82 | ```kotlin 83 | dependencies { 84 | // ... 85 | implementation("net.java.dev.jna:jna:5.12.1") 86 | } 87 | ``` 88 | 89 | [BDK]: https://github.com/bitcoindevkit/ 90 | [`bdk-ffi`]: https://github.com/bitcoindevkit/bdk-ffi 91 | -------------------------------------------------------------------------------- /bdk-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm").version("2.1.10").apply(false) 3 | id("org.gradle.java-library") 4 | id("org.gradle.maven-publish") 5 | id("org.gradle.signing") 6 | id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 7 | id("org.jetbrains.dokka").version("2.0.0").apply(false) 8 | id("org.jetbrains.dokka-javadoc").version("2.0.0").apply(false) 9 | } 10 | 11 | // library version is defined in gradle.properties 12 | val libraryVersion: String by project 13 | 14 | // These properties are required here so that the nexus publish-plugin 15 | // finds a staging profile with the correct group (group is otherwise set as "") 16 | // and knows whether to publish to a SNAPSHOT repository or not 17 | // https://github.com/gradle-nexus/publish-plugin#applying-the-plugin 18 | group = "org.bitcoindevkit" 19 | version = libraryVersion 20 | 21 | nexusPublishing { 22 | repositories { 23 | create("sonatype") { 24 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 25 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 26 | 27 | val ossrhUsername: String? by project 28 | val ossrhPassword: String? by project 29 | username.set(ossrhUsername) 30 | password.set(ossrhPassword) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bdk-jvm/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | android.enableJetifier=true 3 | kotlin.code.style=official 4 | libraryVersion=2.0.0-SNAPSHOT 5 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 6 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 7 | -------------------------------------------------------------------------------- /bdk-jvm/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-jvm/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /bdk-jvm/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /bdk-jvm/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /bdk-jvm/justfile: -------------------------------------------------------------------------------- 1 | [group("Repo")] 2 | [doc("Default command; list all available commands.")] 3 | @list: 4 | just --list --unsorted 5 | 6 | [group("Repo")] 7 | [doc("Open repo on GitHub in your default browser.")] 8 | repo: 9 | open https://github.com/bitcoindevkit/bdk-ffi 10 | 11 | [group("Repo")] 12 | [doc("Build the API docs.")] 13 | docs: 14 | ./gradlew :lib:dokkaGeneratePublicationHtml 15 | 16 | [group("Repo")] 17 | [doc("Publish the library to your local Maven repository.")] 18 | publish-local: 19 | ./gradlew publishToMavenLocal -P localBuild 20 | 21 | [group("Build")] 22 | [doc("Build the library for given ARCH.")] 23 | build ARCH="macos-aarch64": 24 | bash ./scripts/build-{{ARCH}}.sh 25 | 26 | [group("Build")] 27 | [doc("List available architectures for the build command.")] 28 | @list-architectures: 29 | echo "Available architectures:" 30 | echo " - linux-x86_64" 31 | echo " - macos-aarch64" 32 | echo " - macos-x86_64" 33 | echo " - windows-x86_64" 34 | 35 | [group("Build")] 36 | [doc("Remove all caches and previous build directories to start from scratch.")] 37 | clean: 38 | rm -rf ../bdk-ffi/target/ 39 | rm -rf ./build/ 40 | rm -rf ./lib/build/ 41 | 42 | [group("Test")] 43 | [doc("Run all tests, unless a specific test is provided.")] 44 | test *TEST: 45 | ./gradlew test {{ if TEST == "" { "" } else { "--tests " + TEST } }} 46 | 47 | [group("Test")] 48 | [doc("Run only offline tests.")] 49 | test-offline: 50 | ./gradlew test -P excludeConnectedTests 51 | -------------------------------------------------------------------------------- /bdk-jvm/lib/README.md: -------------------------------------------------------------------------------- 1 | # Module bdk-jvm 2 | 3 | The [bitcoindevkit](https://bitcoindevkit.org/) language bindings library for Kotlin on the JVM. 4 | 5 | # Package org.bitcoindevkit 6 | 7 | The functionality exposed in this package is in fact a combination of the [bdk_wallet](https://crates.io/crates/bdk_wallet), [bdk_core](https://crates.io/crates/bdk_core), [bdk_electrum](https://crates.io/crates/bdk_electrum), [bdk_esplora](https://crates.io/crates/bdk_esplora), [bitcoin](https://crates.io/crates/bitcoin), and [miniscript](https://crates.io/crates/miniscript) crates. 8 | -------------------------------------------------------------------------------- /bdk-jvm/lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat.* 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent.* 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | 5 | // library version is defined in gradle.properties 6 | val libraryVersion: String by project 7 | 8 | plugins { 9 | id("org.jetbrains.kotlin.jvm") 10 | id("org.gradle.java-library") 11 | id("org.gradle.maven-publish") 12 | id("org.gradle.signing") 13 | id("org.jetbrains.dokka") 14 | id("org.jetbrains.dokka-javadoc") 15 | } 16 | 17 | java { 18 | sourceCompatibility = JavaVersion.VERSION_11 19 | targetCompatibility = JavaVersion.VERSION_11 20 | withSourcesJar() 21 | withJavadocJar() 22 | } 23 | 24 | tasks.withType { 25 | kotlinOptions { 26 | jvmTarget = "11" 27 | } 28 | } 29 | 30 | // This block ensures that the tests that require access to a blockchain are not 31 | // run if the -P excludeConnectedTests flag is passed to gradle. 32 | // This ensures our CI runs are not fickle by not requiring access to testnet or signet. 33 | // This is a workaround until we have a proper regtest setup for the CI. 34 | // Note that the command in the CI is ./gradlew test -P excludeConnectedTests 35 | tasks.test { 36 | if (project.hasProperty("excludeConnectedTests")) { 37 | exclude("**/LiveElectrumClientTest.class") 38 | exclude("**/LiveMemoryWalletTest.class") 39 | exclude("**/LiveTransactionTest.class") 40 | exclude("**/LiveTxBuilderTest.class") 41 | exclude("**/LiveWalletTest.class") 42 | exclude("**/LiveKyotoTest.class") 43 | } 44 | } 45 | 46 | testing { 47 | suites { 48 | val test by getting(JvmTestSuite::class) { 49 | useKotlinTest("1.9.23") 50 | } 51 | } 52 | } 53 | 54 | tasks.withType { 55 | testLogging { 56 | events(PASSED, SKIPPED, FAILED, STANDARD_OUT, STANDARD_ERROR) 57 | exceptionFormat = FULL 58 | showExceptions = true 59 | showStackTraces = true 60 | showCauses = true 61 | } 62 | } 63 | 64 | dependencies { 65 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 66 | implementation("net.java.dev.jna:jna:5.14.0") 67 | } 68 | 69 | afterEvaluate { 70 | publishing { 71 | publications { 72 | create("maven") { 73 | groupId = "org.bitcoindevkit" 74 | artifactId = "bdk-jvm" 75 | version = libraryVersion 76 | 77 | from(components["java"]) 78 | pom { 79 | name.set("bdk-jvm") 80 | description.set("Bitcoin Dev Kit Kotlin language bindings.") 81 | url.set("https://bitcoindevkit.org") 82 | licenses { 83 | license { 84 | name.set("APACHE 2.0") 85 | url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-APACHE") 86 | } 87 | license { 88 | name.set("MIT") 89 | url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") 90 | } 91 | } 92 | developers { 93 | developer { 94 | id.set("bdkdevelopers") 95 | name.set("Bitcoin Dev Kit Developers") 96 | email.set("dev@bitcoindevkit.org") 97 | } 98 | } 99 | scm { 100 | connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") 101 | developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") 102 | url.set("https://github.com/bitcoindevkit/bdk-ffi/tree/master") 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | signing { 111 | if (project.hasProperty("localBuild")) { 112 | isRequired = false 113 | } 114 | 115 | val signingKeyId: String? by project 116 | val signingKey: String? by project 117 | val signingPassword: String? by project 118 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 119 | sign(publishing.publications) 120 | } 121 | 122 | dokka { 123 | moduleName.set("bdk-jvm") 124 | moduleVersion.set(libraryVersion) 125 | dokkaSourceSets.main { 126 | includes.from("README.md") 127 | sourceLink { 128 | localDirectory.set(file("src/main/kotlin")) 129 | remoteUrl("https://bitcoindevkit.org/") 130 | remoteLineSuffix.set("#L") 131 | } 132 | } 133 | pluginsConfiguration.html { 134 | // customStyleSheets.from("styles.css") 135 | // customAssets.from("logo.svg") 136 | footerMessage.set("(c) Bitcoin Dev Kit Developers") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveElectrumClientTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import org.bitcoindevkit.ServerFeaturesRes 6 | 7 | private const val SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602" 8 | 9 | class LiveElectrumClientTest { 10 | private val descriptor: Descriptor = Descriptor( 11 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 12 | Network.SIGNET 13 | ) 14 | private val changeDescriptor: Descriptor = Descriptor( 15 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 16 | Network.SIGNET 17 | ) 18 | 19 | @Test 20 | fun testSyncedBalance() { 21 | var conn: Persister = Persister.newInMemory() 22 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 23 | val electrumClient: ElectrumClient = ElectrumClient(SIGNET_ELECTRUM_URL) 24 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 25 | val update = electrumClient.fullScan(fullScanRequest, 10uL, 10uL, false) 26 | wallet.applyUpdate(update) 27 | println("Balance: ${wallet.balance().total.toSat()}") 28 | 29 | assert(wallet.balance().total.toSat() > 0uL) { 30 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 31 | } 32 | 33 | println("Transactions count: ${wallet.transactions().count()}") 34 | val transactions = wallet.transactions().take(3) 35 | for (tx in transactions) { 36 | val sentAndReceived = wallet.sentAndReceived(tx.transaction) 37 | println("Transaction: ${tx.transaction.computeTxid()}") 38 | println("Sent ${sentAndReceived.sent.toSat()}") 39 | println("Received ${sentAndReceived.received.toSat()}") 40 | } 41 | } 42 | 43 | @Test 44 | fun testServerFeatures() { 45 | val electrumClient: ElectrumClient = ElectrumClient("ssl://electrum.blockstream.info:60002") 46 | val features: ServerFeaturesRes = electrumClient.serverFeatures() 47 | println("Server Features:\n$features") 48 | 49 | assertEquals( 50 | expected = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", 51 | actual = features.genesisHash.toString() 52 | ) 53 | } 54 | 55 | @Test 56 | fun testBlockSubscription() { 57 | val electrumClient: ElectrumClient = ElectrumClient("ssl://electrum.blockstream.info:60002") 58 | val headerNotification: HeaderNotification = electrumClient.blockHeadersSubscribe() 59 | println("Latest known block:\n$headerNotification") 60 | } 61 | 62 | @Test 63 | fun testPing() { 64 | val electrumClient: ElectrumClient = ElectrumClient("ssl://electrum.blockstream.info:60002") 65 | val ping = electrumClient.ping() 66 | 67 | assert(ping == Unit) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.coroutines.cancelAndJoin 5 | import kotlinx.coroutines.launch 6 | import kotlin.test.Test 7 | import kotlin.test.AfterTest 8 | import kotlin.test.assertNotNull 9 | 10 | import java.nio.file.Files 11 | import java.nio.file.Paths 12 | import kotlin.io.path.ExperimentalPathApi 13 | import kotlin.io.path.deleteRecursively 14 | 15 | class LiveKyotoTest { 16 | private val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", Network.SIGNET) 17 | private val changeDescriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", Network.SIGNET) 18 | private val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u) 19 | private val peer: Peer = Peer(ip, null, false) 20 | private val currentPath = Paths.get(".").toAbsolutePath().normalize() 21 | private val persistenceFilePath = Files.createTempDirectory(currentPath, "tempDirPrefix_") 22 | 23 | @OptIn(ExperimentalPathApi::class) 24 | @AfterTest 25 | fun cleanup() { 26 | persistenceFilePath.deleteRecursively() 27 | } 28 | 29 | @Test 30 | fun testKyoto() { 31 | val conn: Persister = Persister.newInMemory() 32 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 33 | val peers = listOf(peer) 34 | runBlocking { 35 | val lightClient = CbfBuilder() 36 | .peers(peers) 37 | .connections(1u) 38 | .scanType(ScanType.New) 39 | .dataDir(persistenceFilePath.toString()) 40 | .build(wallet) 41 | val client = lightClient.client 42 | val node = lightClient.node 43 | println("Node running") 44 | val logJob = launch { 45 | while (true) { 46 | val log = client.nextLog() 47 | println(log) 48 | } 49 | } 50 | node.run() 51 | val update: Update = client.update() 52 | wallet.applyUpdate(update) 53 | assert(wallet.balance().total.toSat() > 0uL) { 54 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 55 | } 56 | logJob.cancelAndJoin() 57 | client.shutdown() 58 | println("Test completed successfully") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveMemoryWalletTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | 5 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 6 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 7 | 8 | class LiveMemoryWalletTest { 9 | private val descriptor: Descriptor = Descriptor( 10 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 11 | Network.SIGNET 12 | ) 13 | private val changeDescriptor: Descriptor = Descriptor( 14 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 15 | Network.SIGNET 16 | ) 17 | 18 | @Test 19 | fun testSyncedBalance() { 20 | val descriptor: Descriptor = Descriptor( 21 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 22 | Network.SIGNET 23 | ) 24 | var conn: Persister = Persister.newInMemory() 25 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 26 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 27 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 28 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 29 | wallet.applyUpdate(update) 30 | println("Balance: ${wallet.balance().total.toSat()}") 31 | 32 | assert(wallet.balance().total.toSat() > 0uL) { 33 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 34 | } 35 | 36 | println("Transactions count: ${wallet.transactions().count()}") 37 | val transactions = wallet.transactions().take(3) 38 | for (tx in transactions) { 39 | val sentAndReceived = wallet.sentAndReceived(tx.transaction) 40 | println("Transaction: ${tx.transaction.computeTxid()}") 41 | println("Sent ${sentAndReceived.sent.toSat()}") 42 | println("Received ${sentAndReceived.received.toSat()}") 43 | } 44 | } 45 | 46 | @Test 47 | fun testScriptInspector() { 48 | var conn: Persister = Persister.newInMemory() 49 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 50 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 51 | 52 | val scriptInspector: FullScriptInspector = FullScriptInspector() 53 | val fullScanRequest: FullScanRequest = wallet.startFullScan().inspectSpksForAllKeychains(scriptInspector).build() 54 | val update = esploraClient.fullScan(fullScanRequest, 21uL, 1uL) 55 | 56 | wallet.applyUpdate(update) 57 | println("Balance: ${wallet.balance().total.toSat()}") 58 | 59 | assert(wallet.balance().total.toSat() > 0uL) { 60 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 61 | } 62 | } 63 | } 64 | 65 | class FullScriptInspector: FullScanScriptInspector { 66 | override fun inspect(keychain: KeychainKind, index: UInt, script: Script){ 67 | println("Inspecting index $index for keychain $keychain") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTransactionTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | 5 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 6 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 7 | 8 | class LiveTransactionTest { 9 | private val descriptor: Descriptor = Descriptor( 10 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 11 | Network.SIGNET 12 | ) 13 | private val changeDescriptor: Descriptor = Descriptor( 14 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 15 | Network.SIGNET 16 | ) 17 | 18 | @Test 19 | fun testSyncedBalance() { 20 | var conn: Persister = Persister.newInMemory() 21 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) 22 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 23 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 24 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 25 | wallet.applyUpdate(update) 26 | println("Wallet balance: ${wallet.balance().total.toSat()}") 27 | 28 | assert(wallet.balance().total.toSat() > 0uL) { 29 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 30 | } 31 | 32 | val transaction: Transaction = wallet.transactions().first().transaction 33 | println("First transaction:") 34 | println("Txid: ${transaction.computeTxid()}") 35 | println("Version: ${transaction.version()}") 36 | println("Total size: ${transaction.totalSize()}") 37 | println("Vsize: ${transaction.vsize()}") 38 | println("Weight: ${transaction.weight()}") 39 | println("Coinbase transaction: ${transaction.isCoinbase()}") 40 | println("Is explicitly RBF: ${transaction.isExplicitlyRbf()}") 41 | println("Inputs: ${transaction.input()}") 42 | println("Outputs: ${transaction.output()}") 43 | 44 | val blockId = wallet.latestCheckpoint().toString() 45 | println("Latest checkpoint: $blockId") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.AfterTest 4 | import kotlin.test.Test 5 | import kotlin.test.assertTrue 6 | import java.io.File 7 | 8 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 9 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 10 | 11 | class LiveTxBuilderTest { 12 | private val persistenceFilePath = run { 13 | val currentDirectory = System.getProperty("user.dir") 14 | "$currentDirectory/bdk_persistence.sqlite" 15 | } 16 | private val descriptor: Descriptor = Descriptor( 17 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 18 | Network.SIGNET 19 | ) 20 | private val changeDescriptor: Descriptor = Descriptor( 21 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 22 | Network.SIGNET 23 | ) 24 | 25 | @AfterTest 26 | fun cleanup() { 27 | val file = File(persistenceFilePath) 28 | if (file.exists()) { 29 | file.delete() 30 | } 31 | } 32 | 33 | @Test 34 | fun testTxBuilder() { 35 | val connection: Persister = Persister.newSqlite(persistenceFilePath) 36 | val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) 37 | val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) 38 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 39 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 40 | wallet.applyUpdate(update) 41 | wallet.persist(connection) 42 | println("Balance: ${wallet.balance().total.toSat()}") 43 | 44 | assert(wallet.balance().total.toSat() > 0uL) { 45 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 46 | } 47 | 48 | val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 49 | val psbt: Psbt = TxBuilder() 50 | .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL)) 51 | .feeRate(FeeRate.fromSatPerVb(2uL)) 52 | .finish(wallet) 53 | 54 | println(psbt.serialize()) 55 | 56 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 57 | } 58 | 59 | @Test 60 | fun complexTxBuilder() { 61 | val connection: Persister = Persister.newSqlite(persistenceFilePath) 62 | val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) 63 | val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) 64 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 65 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 66 | wallet.applyUpdate(update) 67 | wallet.persist(connection) 68 | println("Balance: ${wallet.balance().total.toSat()}") 69 | 70 | assert(wallet.balance().total.toSat() > 0uL) { 71 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 72 | } 73 | 74 | val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 75 | val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET) 76 | val allRecipients: List = listOf( 77 | ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)), 78 | ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)), 79 | ) 80 | 81 | val psbt: Psbt = TxBuilder() 82 | .setRecipients(allRecipients) 83 | .feeRate(FeeRate.fromSatPerVb(4uL)) 84 | .finish(wallet) 85 | 86 | wallet.sign(psbt) 87 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.AfterTest 4 | import kotlin.test.Test 5 | import kotlin.test.assertTrue 6 | import java.io.File 7 | 8 | private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 9 | private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 10 | 11 | class LiveWalletTest { 12 | private val persistenceFilePath = run { 13 | val currentDirectory = System.getProperty("user.dir") 14 | "$currentDirectory/bdk_persistence.sqlite" 15 | } 16 | private val descriptor: Descriptor = Descriptor( 17 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 18 | Network.SIGNET 19 | ) 20 | private val changeDescriptor: Descriptor = Descriptor( 21 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 22 | Network.SIGNET 23 | ) 24 | 25 | @AfterTest 26 | fun cleanup() { 27 | val file = File(persistenceFilePath) 28 | if (file.exists()) { 29 | file.delete() 30 | } 31 | } 32 | 33 | @Test 34 | fun testSyncedBalance() { 35 | val connection = Persister.newSqlite(persistenceFilePath) 36 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) 37 | val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) 38 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 39 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 40 | wallet.applyUpdate(update) 41 | wallet.persist(connection) 42 | println("Balance: ${wallet.balance().total.toSat()}") 43 | 44 | assert(wallet.balance().total.toSat() > 0uL) { 45 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 46 | } 47 | 48 | println("Transactions count: ${wallet.transactions().count()}") 49 | val transactions = wallet.transactions().take(3) 50 | for (tx in transactions) { 51 | val sentAndReceived = wallet.sentAndReceived(tx.transaction) 52 | println("Transaction: ${tx.transaction.computeTxid()}") 53 | println("Sent ${sentAndReceived.sent.toSat()}") 54 | println("Received ${sentAndReceived.received.toSat()}") 55 | } 56 | } 57 | 58 | @Test 59 | fun testBroadcastTransaction() { 60 | val connection = Persister.newSqlite(persistenceFilePath) 61 | val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) 62 | val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) 63 | val fullScanRequest: FullScanRequest = wallet.startFullScan().build() 64 | val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) 65 | wallet.applyUpdate(update) 66 | wallet.persist(connection) 67 | println("Balance: ${wallet.balance().total.toSat()}") 68 | 69 | assert(wallet.balance().total.toSat() > 0uL) { 70 | "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." 71 | } 72 | 73 | val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET) 74 | 75 | val psbt: Psbt = TxBuilder() 76 | .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL)) 77 | .feeRate(FeeRate.fromSatPerVb(2uL)) 78 | .finish(wallet) 79 | 80 | println(psbt.serialize()) 81 | assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") 82 | 83 | val walletDidSign = wallet.sign(psbt) 84 | assertTrue(walletDidSign) 85 | 86 | val tx: Transaction = psbt.extractTx() 87 | println("Txid is: ${tx.computeTxid()}") 88 | 89 | val txFee: Amount = wallet.calculateFee(tx) 90 | println("Tx fee is: ${txFee.toSat()}") 91 | 92 | val feeRate: FeeRate = wallet.calculateFeeRate(tx) 93 | println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB") 94 | 95 | esploraClient.broadcast(tx) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineDescriptorTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class OfflineDescriptorTest { 7 | @Test 8 | fun testDescriptorBip86() { 9 | val mnemonic: Mnemonic = Mnemonic.fromString("space echo position wrist orient erupt relief museum myself grain wisdom tumble") 10 | val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) 11 | val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) 12 | 13 | assertEquals( 14 | expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", 15 | actual = descriptor.toString() 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflinePersistenceTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class OfflinePersistenceTest { 7 | private val persistenceFilePath = run { 8 | val currentDirectory = System.getProperty("user.dir") 9 | val dbFileName = "pre_existing_wallet_persistence_test.sqlite" 10 | "$currentDirectory/src/test/resources/$dbFileName" 11 | } 12 | private val descriptor: Descriptor = Descriptor( 13 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", 14 | Network.SIGNET 15 | ) 16 | private val changeDescriptor: Descriptor = Descriptor( 17 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", 18 | Network.SIGNET 19 | ) 20 | 21 | @Test 22 | fun testPersistence() { 23 | val connection = Persister.newSqlite(persistenceFilePath) 24 | 25 | val wallet: Wallet = Wallet.load( 26 | descriptor, 27 | changeDescriptor, 28 | connection 29 | ) 30 | val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) 31 | println("Address: $addressInfo") 32 | 33 | assertEquals( 34 | expected = 7u, 35 | actual = addressInfo.index, 36 | ) 37 | assertEquals( 38 | expected = "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2", 39 | actual = addressInfo.address.toString(), 40 | ) 41 | } 42 | 43 | @Test 44 | fun testPersistenceWithDescriptor() { 45 | val connection = Persister.newSqlite(persistenceFilePath) 46 | 47 | val descriptorPub = Descriptor( 48 | "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/0/*)#zpaanzgu", 49 | Network.SIGNET 50 | ) 51 | val changeDescriptorPub = Descriptor( 52 | "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/1/*)#n4cuwhcy", 53 | Network.SIGNET 54 | ) 55 | 56 | val wallet: Wallet = Wallet.load( 57 | descriptorPub, 58 | changeDescriptorPub, 59 | connection 60 | ) 61 | val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) 62 | println("Address: $addressInfo") 63 | 64 | assertEquals( 65 | expected = 7u, 66 | actual = addressInfo.index, 67 | ) 68 | assertEquals( 69 | expected = "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2", 70 | actual = addressInfo.address.toString(), 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoindevkit 2 | 3 | import kotlin.test.AfterTest 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | import kotlin.test.assertFalse 8 | import java.io.File 9 | 10 | class OfflineWalletTest { 11 | private val descriptor: Descriptor = Descriptor( 12 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 13 | Network.TESTNET 14 | ) 15 | private val changeDescriptor: Descriptor = Descriptor( 16 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 17 | Network.TESTNET 18 | ) 19 | 20 | @Test 21 | fun testDescriptorBip86() { 22 | val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12) 23 | val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) 24 | val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) 25 | 26 | assertTrue(descriptor.toString().startsWith("tr"), "Bip86 Descriptor does not start with 'tr'") 27 | } 28 | 29 | @Test 30 | fun testNewAddress() { 31 | var conn: Persister = Persister.newInMemory() 32 | val wallet: Wallet = Wallet( 33 | descriptor, 34 | changeDescriptor, 35 | Network.TESTNET, 36 | conn 37 | ) 38 | val addressInfo: AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) 39 | 40 | assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") 41 | assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") 42 | assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") 43 | assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") 44 | 45 | assertEquals( 46 | expected = "tb1qhjys9wxlfykmte7ftryptx975uqgd6kcm6a7z4", 47 | actual = addressInfo.address.toString() 48 | ) 49 | } 50 | 51 | @Test 52 | fun testBalance() { 53 | var conn: Persister = Persister.newInMemory() 54 | val wallet: Wallet = Wallet( 55 | descriptor, 56 | changeDescriptor, 57 | Network.TESTNET, 58 | conn 59 | ) 60 | 61 | assertEquals( 62 | expected = 0uL, 63 | actual = wallet.balance().total.toSat() 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bdk-jvm/lib/src/test/resources/pre_existing_wallet_persistence_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-jvm/lib/src/test/resources/pre_existing_wallet_persistence_test.sqlite -------------------------------------------------------------------------------- /bdk-jvm/scripts/build-linux-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMPILATION_TARGET="x86_64-unknown-linux-gnu" 4 | TARGET_DIR="target/x86_64-unknown-linux-gnu/release-smaller" 5 | RESOURCE_DIR="resources/linux-x86-64" 6 | LIB_NAME="libbdkffi.so" 7 | 8 | # Move to the Rust library directory 9 | cd ../bdk-ffi/ || exit 10 | 11 | # Build the Rust library 12 | rustup default 1.84.1 13 | rustup target add $COMPILATION_TARGET 14 | cargo build --profile release-smaller --target $COMPILATION_TARGET 15 | 16 | # Generate Kotlin bindings using uniffi-bindgen 17 | cargo run --bin uniffi-bindgen generate --library ./$TARGET_DIR/$LIB_NAME --language kotlin --out-dir ../bdk-jvm/lib/src/main/kotlin/ --no-format 18 | 19 | # Copy the binary to the resources directory 20 | mkdir -p ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 21 | cp ./$TARGET_DIR/$LIB_NAME ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 22 | -------------------------------------------------------------------------------- /bdk-jvm/scripts/build-macos-aarch64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMPILATION_TARGET="aarch64-apple-darwin" 4 | TARGET_DIR="target/$COMPILATION_TARGET/release-smaller" 5 | RESOURCE_DIR="resources/darwin-aarch64" 6 | LIB_NAME="libbdkffi.dylib" 7 | 8 | # Move to the Rust library directory 9 | cd ../bdk-ffi/ || exit 10 | 11 | # Build the Rust library 12 | rustup default 1.84.1 13 | rustup target add $COMPILATION_TARGET 14 | cargo build --profile release-smaller --target $COMPILATION_TARGET 15 | 16 | # Generate Kotlin bindings using uniffi-bindgen 17 | cargo run --bin uniffi-bindgen generate --library ./$TARGET_DIR/$LIB_NAME --language kotlin --out-dir ../bdk-jvm/lib/src/main/kotlin/ --no-format 18 | 19 | # Copy the binary to the resources directory 20 | mkdir -p ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 21 | cp ./$TARGET_DIR/$LIB_NAME ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 22 | -------------------------------------------------------------------------------- /bdk-jvm/scripts/build-macos-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMPILATION_TARGET="x86_64-apple-darwin" 4 | TARGET_DIR="target/x86_64-apple-darwin/release-smaller" 5 | RESOURCE_DIR="resources/darwin-x86-64" 6 | LIB_NAME="libbdkffi.dylib" 7 | 8 | # Move to the Rust library directory 9 | cd ../bdk-ffi/ || exit 10 | 11 | # Build the Rust library 12 | rustup default 1.84.1 13 | rustup target add $COMPILATION_TARGET 14 | cargo build --profile release-smaller --target $COMPILATION_TARGET 15 | 16 | # Generate Kotlin bindings using uniffi-bindgen 17 | cargo run --bin uniffi-bindgen generate --library ./$TARGET_DIR/$LIB_NAME --language kotlin --out-dir ../bdk-jvm/lib/src/main/kotlin/ --no-format 18 | 19 | # Copy the binary to the resources directory 20 | mkdir -p ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 21 | cp ./$TARGET_DIR/$LIB_NAME ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 22 | -------------------------------------------------------------------------------- /bdk-jvm/scripts/build-windows-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMPILATION_TARGET="x86_64-pc-windows-msvc" 4 | TARGET_DIR="target/x86_64-pc-windows-msvc/release-smaller" 5 | RESOURCE_DIR="resources/win32-x86-64" 6 | LIB_NAME="bdkffi.dll" 7 | 8 | # Move to the Rust library directory 9 | cd ../bdk-ffi/ || exit 10 | 11 | # Build the Rust library 12 | rustup default 1.84.1 13 | rustup target add $COMPILATION_TARGET 14 | cargo build --profile release-smaller --target $COMPILATION_TARGET 15 | 16 | # Generate Kotlin bindings using uniffi-bindgen 17 | cargo run --bin uniffi-bindgen generate --library ./$TARGET_DIR/$LIB_NAME --language kotlin --out-dir ../bdk-jvm/lib/src/main/kotlin/ --no-format 18 | 19 | # Copy the binary to the resources directory 20 | mkdir -p ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 21 | cp ./$TARGET_DIR/$LIB_NAME ../bdk-jvm/lib/src/main/$RESOURCE_DIR/ 22 | -------------------------------------------------------------------------------- /bdk-jvm/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bdk-jvm" 2 | 3 | include(":lib") 4 | 5 | pluginManagement { 6 | repositories { 7 | gradlePluginPortal() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositories { 13 | mavenCentral() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bdk-python/.gitignore: -------------------------------------------------------------------------------- 1 | .tox/ 2 | dist/ 3 | bdkpython.egg-info/ 4 | __pycache__/ 5 | libbdkffi.dylib 6 | .idea/ 7 | .DS_Store 8 | 9 | *.swp 10 | 11 | src/bdkpython/bdk.py 12 | src/bdkpython/*.so 13 | *.whl 14 | build/ 15 | 16 | testing-setup-py-simple-example.py 17 | localenvironment/ 18 | -------------------------------------------------------------------------------- /bdk-python/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ./src/bdkpython/libbdkffi.dylib 2 | include ./src/bdkpython/libbdkffi.so 3 | include ./src/bdkpython/bdkffi.dll 4 | -------------------------------------------------------------------------------- /bdk-python/README.md: -------------------------------------------------------------------------------- 1 | # bdk-python 2 | 3 | The Python language bindings for the [bitcoindevkit](https://github.com/bitcoindevkit). 4 | 5 | See the [package on PyPI](https://pypi.org/project/bdkpython/). 6 | 7 | ## Install from PyPI 8 | 9 | Install the latest release using 10 | 11 | ```shell 12 | pip install bdkpython 13 | ``` 14 | 15 | ## Run the tests 16 | 17 | ```shell 18 | pip install --requirement requirements.txt 19 | bash ./scripts/generate-linux.sh # here you should run the script appropriate for your platform 20 | python setup.py bdist_wheel --verbose 21 | pip install ./dist/bdkpython-.whl --force-reinstall 22 | python -m unittest --verbose 23 | ``` 24 | 25 | ## Build the package 26 | 27 | ```shell 28 | # Install dependencies 29 | pip install --requirement requirements.txt 30 | 31 | # Generate the bindings (use the script appropriate for your platform) 32 | bash ./scripts/generate-linux.sh 33 | 34 | # Build the wheel 35 | python setup.py --verbose bdist_wheel 36 | ``` 37 | 38 | ## Install locally 39 | 40 | ```shell 41 | pip install ./dist/bdkpython-.whl 42 | ``` 43 | -------------------------------------------------------------------------------- /bdk-python/justfile: -------------------------------------------------------------------------------- 1 | [group("Repo")] 2 | [doc("Default command; list all available commands.")] 3 | @list: 4 | just --list --unsorted 5 | 6 | [group("Repo")] 7 | [doc("Open repo on GitHub in your default browser.")] 8 | repo: 9 | open https://github.com/bitcoindevkit/bdk-ffi 10 | 11 | [group("Build")] 12 | [doc("Remove all caches and previous builds to start from scratch.")] 13 | clean: 14 | rm -rf ../bdk-ffi/target/ 15 | rm -rf ./bdkpython.egg-info/ 16 | rm -rf ./build/ 17 | rm -rf ./dist/ 18 | 19 | [group("Test")] 20 | [doc("Run all tests.")] 21 | test: 22 | python3 -m unittest --verbose 23 | -------------------------------------------------------------------------------- /bdk-python/nix/uniffi_bindgen.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | rustPlatform.buildRustPackage rec { 4 | pname = "uniffi_bindgen"; 5 | version = "0.15.2"; 6 | src = fetchFromGitHub { 7 | owner = "mozilla"; 8 | repo = "uniffi-rs"; 9 | rev = "6fa9c06a394b4e9b219fa30fc94e353d17f86e11"; 10 | # rev = "refs/tags/v0.14.1"; 11 | sha256 = "1chahy1ac1r88drpslln2p1b04cbg79ylpxzyyp92s1z7ldm5ddb"; # 0.15.2 12 | # sha256 = "1mff3f3fqqzqx1yv70ff1yzdnvbd90vg2r477mzzcgisg1wfpwi0"; # 0.14.1 13 | fetchSubmodules = true; 14 | } + "/uniffi_bindgen/"; 15 | 16 | doCheck = false; 17 | cargoSha256 = "sha256:08gg285fq8i32nf9kd8s0nn0niacd7sg8krv818nx41i18sm2cf3"; # 0.15.2 18 | # cargoSha256 = "sha256:01zp3rwlni988h02dqhkhzhwccs7bhwc1alhbf6gbw3av4b0m9cf"; # 0.14.1 19 | cargoPatches = [ ./uniffi_0.15.2_cargo_lock.patch ]; 20 | } 21 | -------------------------------------------------------------------------------- /bdk-python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "setuptools-rust"] 3 | 4 | [tool.pytest.ini_options] 5 | pythonpath = [ 6 | "." 7 | ] -------------------------------------------------------------------------------- /bdk-python/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==7.1.2 2 | tox==3.25.1 3 | -------------------------------------------------------------------------------- /bdk-python/requirements.txt: -------------------------------------------------------------------------------- 1 | semantic-version==2.9.0 2 | typing_extensions==4.0.1 3 | setuptools==75.3.2 4 | wheel==0.38.4 5 | -------------------------------------------------------------------------------- /bdk-python/scripts/generate-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | ${PYBIN}/python --version 5 | ${PYBIN}/pip install -r requirements.txt 6 | 7 | cd ../bdk-ffi/ 8 | rustup default 1.84.1 9 | 10 | echo "Generating native binaries..." 11 | cargo build --profile release-smaller 12 | 13 | echo "Generating bdk.py..." 14 | cargo run --bin uniffi-bindgen generate --library ./target/release-smaller/libbdkffi.so --language python --out-dir ../bdk-python/src/bdkpython/ --no-format 15 | 16 | echo "Copying linux libbdkffi.so..." 17 | cp ./target/release-smaller/libbdkffi.so ../bdk-python/src/bdkpython/libbdkffi.so 18 | 19 | echo "All done!" 20 | -------------------------------------------------------------------------------- /bdk-python/scripts/generate-macos-arm64.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | python3 --version 5 | pip install -r requirements.txt 6 | 7 | cd ../bdk-ffi/ 8 | rustup default 1.84.1 9 | rustup target add aarch64-apple-darwin 10 | 11 | echo "Generating native binaries..." 12 | cargo build --profile release-smaller --target aarch64-apple-darwin 13 | 14 | echo "Generating bdk.py..." 15 | 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 16 | 17 | echo "Copying libraries libbdkffi.dylib..." 18 | cp ./target/aarch64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib 19 | 20 | echo "All done!" 21 | -------------------------------------------------------------------------------- /bdk-python/scripts/generate-macos-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | python3 --version 5 | pip install -r requirements.txt 6 | 7 | cd ../bdk-ffi/ 8 | rustup default 1.84.1 9 | rustup target add x86_64-apple-darwin 10 | 11 | echo "Generating native binaries..." 12 | cargo build --profile release-smaller --target x86_64-apple-darwin 13 | 14 | echo "Generating bdk.py..." 15 | 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 16 | 17 | echo "Copying libraries libbdkffi.dylib..." 18 | cp ./target/x86_64-apple-darwin/release-smaller/libbdkffi.dylib ../bdk-python/src/bdkpython/libbdkffi.dylib 19 | 20 | echo "All done!" 21 | -------------------------------------------------------------------------------- /bdk-python/scripts/generate-windows.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | python3 --version 5 | pip install -r requirements.txt 6 | 7 | cd ../bdk-ffi/ 8 | rustup default 1.84.1 9 | rustup target add x86_64-pc-windows-msvc 10 | 11 | echo "Generating native binaries..." 12 | cargo build --profile release-smaller --target x86_64-pc-windows-msvc 13 | 14 | echo "Generating bdk.py..." 15 | 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 16 | 17 | echo "Copying libraries bdkffi.dll..." 18 | cp ./target/x86_64-pc-windows-msvc/release-smaller/bdkffi.dll ../bdk-python/src/bdkpython/bdkffi.dll 19 | 20 | echo "All done!" 21 | -------------------------------------------------------------------------------- /bdk-python/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | LONG_DESCRIPTION = """# bdkpython 6 | The Python language bindings for the [Bitcoin Dev Kit](https://github.com/bitcoindevkit). 7 | 8 | ## Install the package 9 | ```shell 10 | pip install bdkpython 11 | ``` 12 | 13 | ## Simple example 14 | ```python 15 | from bdkpython import Wallet 16 | ``` 17 | """ 18 | 19 | setup( 20 | name="bdkpython", 21 | version="2.0.0.dev0", 22 | description="The Python language bindings for the Bitcoin Development Kit", 23 | long_description=LONG_DESCRIPTION, 24 | long_description_content_type="text/markdown", 25 | include_package_data = True, 26 | zip_safe=False, 27 | packages=["bdkpython"], 28 | package_dir={"bdkpython": "./src/bdkpython"}, 29 | url="https://github.com/bitcoindevkit/bdk-ffi", 30 | author="Bitcoin Dev Kit Developers ", 31 | license="MIT or Apache 2.0", 32 | # This is required to ensure the library name includes the python version, abi, and platform tags 33 | # See issue #350 for more information 34 | has_ext_modules=lambda: True, 35 | ) 36 | -------------------------------------------------------------------------------- /bdk-python/shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | mkShell { 4 | name = "bdk-python-shell"; 5 | packages = [ ( import ./nix/uniffi_bindgen.nix ) ]; 6 | buildInputs = with python37.pkgs; [ 7 | pip 8 | setuptools 9 | ]; 10 | shellHook = '' 11 | export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH 12 | alias pip="PIP_PREFIX='$(pwd)/_build/pip_packages' \pip" 13 | export PYTHONPATH="$(pwd)/_build/pip_packages/lib/python3.7/site-packages:$(pwd):$PYTHONPATH" 14 | export PATH="$(pwd)/_build/pip_packages/bin:$PATH" 15 | unset SOURCE_DATE_EPOCH 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /bdk-python/src/bdkpython/__init__.py: -------------------------------------------------------------------------------- 1 | from bdkpython.bdk import * -------------------------------------------------------------------------------- /bdk-python/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-python/tests/__init__.py -------------------------------------------------------------------------------- /bdk-python/tests/test_live_kyoto.py: -------------------------------------------------------------------------------- 1 | from bdkpython import Persister, Network, Descriptor, KeychainKind, CbfBuilder, CbfComponents, CbfClient, CbfNode, CbfError, IpAddress, ScanType, Peer, Update, Wallet 2 | import unittest 3 | import os 4 | import asyncio 5 | 6 | network: Network = Network.SIGNET 7 | 8 | ip: IpAddress = IpAddress.from_ipv4(68, 47, 229, 218) 9 | peer: Peer = Peer(address=ip, port=None, v2_transport=False) 10 | 11 | descriptor: Descriptor = Descriptor( 12 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 13 | network=network 14 | ) 15 | change_descriptor: Descriptor = Descriptor( 16 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 17 | network=network 18 | ) 19 | 20 | class LiveKyotoTest(unittest.IsolatedAsyncioTestCase): 21 | 22 | def tearDown(self) -> None: 23 | if os.path.exists("./bdk_persistence.sqlite"): 24 | os.remove("./bdk_persistence.sqlite") 25 | if os.path.exists("./data/signet/headers.db"): 26 | os.remove("./data/signet/headers.db") 27 | if os.path.exists("./data/signet/peers.db"): 28 | os.remove("./data/signet/peers.db") 29 | 30 | async def testKyoto(self) -> None: 31 | persister: Persister = Persister.new_in_memory() 32 | wallet: Wallet = Wallet( 33 | descriptor, 34 | change_descriptor, 35 | network, 36 | persister 37 | ) 38 | peers = [peer] 39 | light_client: CbfComponents = CbfBuilder().scan_type(ScanType.NEW()).peers(peers).connections(1).build(wallet) 40 | client: CbfClient = light_client.client 41 | node: CbfNode = light_client.node 42 | async def log_loop(client: CbfClient): 43 | while True: 44 | log = await client.next_log() 45 | print(log) 46 | log_task = asyncio.create_task(log_loop(client)) 47 | node.run() 48 | try: 49 | update: Update = await client.update() 50 | wallet.apply_update(update) 51 | self.assertGreater( 52 | wallet.balance().total.to_sat(), 53 | 0, 54 | f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." 55 | ) 56 | log_task.cancel() 57 | client.shutdown() 58 | except CbfError as e: 59 | raise e 60 | 61 | if __name__ == "__main__": 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /bdk-python/tests/test_live_tx_builder.py: -------------------------------------------------------------------------------- 1 | from bdkpython import Descriptor 2 | from bdkpython import KeychainKind 3 | from bdkpython import Wallet 4 | from bdkpython import EsploraClient 5 | from bdkpython import ScriptAmount 6 | from bdkpython import FullScanRequest 7 | from bdkpython import Address 8 | from bdkpython import Psbt 9 | from bdkpython import TxBuilder 10 | from bdkpython import Persister 11 | from bdkpython import Network 12 | from bdkpython import Amount 13 | from bdkpython import FeeRate 14 | 15 | import unittest 16 | import os 17 | 18 | SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 19 | TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 20 | 21 | descriptor: Descriptor = Descriptor( 22 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 23 | Network.TESTNET 24 | ) 25 | change_descriptor: Descriptor = Descriptor( 26 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 27 | Network.TESTNET 28 | ) 29 | 30 | class LiveTxBuilderTest(unittest.TestCase): 31 | 32 | def tearDown(self) -> None: 33 | if os.path.exists("./bdk_persistence.sqlite"): 34 | os.remove("./bdk_persistence.sqlite") 35 | 36 | def test_tx_builder(self): 37 | connection: Persister = Persister.new_in_memory() 38 | wallet: Wallet = Wallet( 39 | descriptor, 40 | change_descriptor, 41 | Network.SIGNET, 42 | connection 43 | ) 44 | esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) 45 | full_scan_request: FullScanRequest = wallet.start_full_scan().build() 46 | update = esplora_client.full_scan( 47 | request=full_scan_request, 48 | stop_gap=10, 49 | parallel_requests=1 50 | ) 51 | wallet.apply_update(update) 52 | 53 | self.assertGreater( 54 | wallet.balance().total.to_sat(), 55 | 0, 56 | f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." 57 | ) 58 | 59 | recipient = Address( 60 | address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", 61 | network=Network.SIGNET 62 | ) 63 | 64 | psbt = TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=Amount.from_sat(4200)).fee_rate(fee_rate=FeeRate.from_sat_per_vb(2)).finish(wallet) 65 | 66 | self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") 67 | 68 | def complex_tx_builder(self): 69 | persister: Persister = Persister.new_in_memory() 70 | wallet: Wallet = Wallet( 71 | descriptor, 72 | change_descriptor, 73 | Network.SIGNET, 74 | persister 75 | ) 76 | esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) 77 | full_scan_request: FullScanRequest = wallet.start_full_scan().build() 78 | update = esplora_client.full_scan( 79 | request=full_scan_request, 80 | stop_gap=10, 81 | parallel_requests=1 82 | ) 83 | wallet.apply_update(update) 84 | 85 | self.assertGreater( 86 | wallet.balance().total.to_sat(), 87 | 0, 88 | f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." 89 | ) 90 | 91 | recipient1 = Address( 92 | address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", 93 | network=Network.SIGNET 94 | ) 95 | recipient2 = Address( 96 | address="tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", 97 | network=Network.SIGNET 98 | ) 99 | all_recipients = list( 100 | ScriptAmount(recipient1.script_pubkey, 4200), 101 | ScriptAmount(recipient2.script_pubkey, 4200) 102 | ) 103 | 104 | psbt: Psbt = TxBuilder().set_recipients(all_recipients).fee_rate(fee_rate=FeeRate.from_sat_per_vb(2)).finish(wallet) 105 | wallet.sign(psbt) 106 | 107 | self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /bdk-python/tests/test_live_wallet.py: -------------------------------------------------------------------------------- 1 | from bdkpython import Descriptor 2 | from bdkpython import KeychainKind 3 | from bdkpython import Wallet 4 | from bdkpython import EsploraClient 5 | from bdkpython import FullScanRequest 6 | from bdkpython import Address 7 | from bdkpython import Psbt 8 | from bdkpython import TxBuilder 9 | from bdkpython import Persister 10 | from bdkpython import Network 11 | from bdkpython import Amount 12 | from bdkpython import FeeRate 13 | 14 | import unittest 15 | import os 16 | 17 | SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 18 | TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 19 | 20 | descriptor: Descriptor = Descriptor( 21 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 22 | Network.TESTNET 23 | ) 24 | change_descriptor: Descriptor = Descriptor( 25 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 26 | Network.TESTNET 27 | ) 28 | 29 | class LiveWalletTest(unittest.TestCase): 30 | 31 | def tearDown(self) -> None: 32 | if os.path.exists("./bdk_persistence.sqlite"): 33 | os.remove("./bdk_persistence.sqlite") 34 | 35 | def test_synced_balance(self): 36 | persister: Persister = Persister.new_in_memory() 37 | wallet: Wallet = Wallet( 38 | descriptor, 39 | change_descriptor, 40 | Network.SIGNET, 41 | persister 42 | ) 43 | esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) 44 | full_scan_request: FullScanRequest = wallet.start_full_scan().build() 45 | update = esplora_client.full_scan( 46 | request=full_scan_request, 47 | stop_gap=10, 48 | parallel_requests=1 49 | ) 50 | wallet.apply_update(update) 51 | 52 | self.assertGreater( 53 | wallet.balance().total.to_sat(), 54 | 0, 55 | f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." 56 | ) 57 | 58 | print(f"Transactions count: {len(wallet.transactions())}") 59 | transactions = wallet.transactions()[:3] 60 | for tx in transactions: 61 | sent_and_received = wallet.sent_and_received(tx.transaction) 62 | print(f"Transaction: {tx.transaction.compute_txid()}") 63 | print(f"Sent {sent_and_received.sent.to_sat()}") 64 | print(f"Received {sent_and_received.received.to_sat()}") 65 | 66 | 67 | def test_broadcast_transaction(self): 68 | persister: Persister = Persister.new_in_memory() 69 | wallet: Wallet = Wallet( 70 | descriptor, 71 | change_descriptor, 72 | Network.SIGNET, 73 | persister 74 | ) 75 | esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) 76 | full_scan_request: FullScanRequest = wallet.start_full_scan().build() 77 | update = esplora_client.full_scan( 78 | request=full_scan_request, 79 | stop_gap=10, 80 | parallel_requests=1 81 | ) 82 | wallet.apply_update(update) 83 | 84 | self.assertGreater( 85 | wallet.balance().total.to_sat(), 86 | 0, 87 | f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." 88 | ) 89 | 90 | recipient = Address( 91 | address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", 92 | network=Network.SIGNET 93 | ) 94 | 95 | psbt: Psbt = TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=Amount.from_sat(4200)).fee_rate(fee_rate=FeeRate.from_sat_per_vb(2)).finish(wallet) 96 | self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") 97 | 98 | walletDidSign = wallet.sign(psbt) 99 | self.assertTrue(walletDidSign) 100 | tx = psbt.extract_tx() 101 | print(f"Transaction Id: {tx.compute_txid()}") 102 | fee = wallet.calculate_fee(tx) 103 | print(f"Transaction Fee: {fee.to_sat()}") 104 | fee_rate = wallet.calculate_fee_rate(tx) 105 | print(f"Transaction Fee Rate: {fee_rate.to_sat_per_vb_ceil()} sat/vB") 106 | 107 | esplora_client.broadcast(tx) 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /bdk-python/tests/test_offline_descriptor.py: -------------------------------------------------------------------------------- 1 | from bdkpython import Descriptor 2 | from bdkpython import Mnemonic 3 | from bdkpython import DescriptorSecretKey 4 | from bdkpython import KeychainKind 5 | from bdkpython import Network 6 | 7 | import unittest 8 | 9 | class OfflineDescriptorTest(unittest.TestCase): 10 | 11 | def test_descriptor_bip86(self): 12 | mnemonic: Mnemonic = Mnemonic.from_string("space echo position wrist orient erupt relief museum myself grain wisdom tumble") 13 | descriptor_secret_key: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, None) 14 | descriptor: Descriptor = Descriptor.new_bip86(descriptor_secret_key, KeychainKind.EXTERNAL, Network.TESTNET) 15 | 16 | self.assertEqual( 17 | "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", 18 | descriptor.__str__() 19 | ) 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /bdk-python/tests/test_offline_wallet.py: -------------------------------------------------------------------------------- 1 | from bdkpython import Descriptor 2 | from bdkpython import Wallet 3 | from bdkpython import KeychainKind 4 | from bdkpython import Persister 5 | from bdkpython import AddressInfo 6 | from bdkpython import Network 7 | 8 | import unittest 9 | import os 10 | 11 | descriptor: Descriptor = Descriptor( 12 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 13 | Network.TESTNET 14 | ) 15 | change_descriptor: Descriptor = Descriptor( 16 | "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 17 | Network.TESTNET 18 | ) 19 | 20 | class OfflineWalletTest(unittest.TestCase): 21 | 22 | def tearDown(self) -> None: 23 | if os.path.exists("./bdk_persistence.sqlite"): 24 | os.remove("./bdk_persistence.sqlite") 25 | 26 | def test_new_address(self): 27 | persister: Persister = Persister.new_in_memory() 28 | wallet: Wallet = Wallet( 29 | descriptor, 30 | change_descriptor, 31 | Network.TESTNET, 32 | persister 33 | ) 34 | address_info: AddressInfo = wallet.reveal_next_address(KeychainKind.EXTERNAL) 35 | 36 | self.assertTrue(address_info.address.is_valid_for_network(Network.TESTNET), "Address is not valid for testnet network") 37 | self.assertTrue(address_info.address.is_valid_for_network(Network.SIGNET), "Address is not valid for signet network") 38 | self.assertFalse(address_info.address.is_valid_for_network(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") 39 | self.assertFalse(address_info.address.is_valid_for_network(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") 40 | 41 | self.assertEqual("tb1qhjys9wxlfykmte7ftryptx975uqgd6kcm6a7z4", address_info.address.__str__()) 42 | 43 | def test_balance(self): 44 | persister: Persister = Persister.new_in_memory() 45 | wallet: Wallet = Wallet( 46 | descriptor, 47 | change_descriptor, 48 | Network.TESTNET, 49 | persister 50 | ) 51 | 52 | self.assertEqual(wallet.balance().total.to_sat(), 0) 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /bdk-swift/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "bdk-swift", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "BitcoinDevKit", 16 | targets: ["bdkFFI", "BitcoinDevKit"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | // .binaryTarget( 26 | // name: "bdkFFI", 27 | // url: "https://github.com/bitcoindevkit/bdk-swift/releases/download/0.3.0/bdkFFI.xcframework.zip", 28 | // checksum: "7d4a2fdeb03fb3eff107e45ee3148dd9b67966406c82d6e3c19f653c27180cfd"), 29 | .binaryTarget(name: "bdkFFI", path: "./bdkFFI.xcframework"), 30 | .target( 31 | name: "BitcoinDevKit", 32 | dependencies: ["bdkFFI"] 33 | ), 34 | .testTarget( 35 | name: "BitcoinDevKitTests", 36 | dependencies: ["BitcoinDevKit"], 37 | resources: [ 38 | .copy("Resources/pre_existing_wallet_persistence_test.sqlite") 39 | ] 40 | ), 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /bdk-swift/Package.swift.txt: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "bdk-swift", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "BitcoinDevKit", 16 | targets: ["bdkFFI", "BitcoinDevKit"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .binaryTarget( 26 | name: "bdkFFI", 27 | url: "BDKFFIURL", 28 | checksum: "BDKFFICHECKSUM"), 29 | .target( 30 | name: "BitcoinDevKit", 31 | dependencies: ["bdkFFI"]), 32 | .testTarget( 33 | name: "BitcoinDevKitTests", 34 | dependencies: ["BitcoinDevKit"]), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /bdk-swift/README.md: -------------------------------------------------------------------------------- 1 | # bdk-swift 2 | 3 | This project builds a Swift package that provides [Swift] language bindings for the 4 | [BDK] libraries. The Swift language bindings are created by the [`bdk-ffi`] project which is included as a module of this repository. 5 | 6 | Supported target platforms are: 7 | 8 | - macOS, X86_64 and M1 (aarch64) 9 | - iOS, iPhones (aarch64) 10 | - iOS simulator, X86_64 and M1 (aarch64) 11 | 12 | ## How to Use 13 | 14 | To use the Swift language bindings for [BDK] in your [Xcode] iOS or macOS project add 15 | the GitHub repository https://github.com/bitcoindevkit/bdk-swift and select one of the 16 | release versions. You may then import and use the `BitcoinDevKit` library in your Swift 17 | code. For example: 18 | 19 | ```swift 20 | import BitcoinDevKit 21 | 22 | ... 23 | 24 | ``` 25 | 26 | Swift Package Manager releases for `bdk-swift` are published to a separate repository (https://github.com/bitcoindevkit/bdk-swift), and that is where the releases are created for it. 27 | 28 | The `bdk-swift/build-xcframework.sh` script can be used instead to create a version of the project for local testing. 29 | 30 | ### How to test 31 | 32 | ```shell 33 | swift test 34 | ``` 35 | 36 | ### Example Projects 37 | 38 | - [BDKSwiftExampleWallet](https://github.com/bitcoindevkit/BDKSwiftExampleWallet), iOS 39 | 40 | ## How to Build and Publish 41 | 42 | If you are a maintainer of this project or want to build and publish this project to your 43 | own GitHub repository use the following steps: 44 | 45 | 1. If it doesn't already exist, create a new `release/0.MINOR` branch from the `master` branch. 46 | 2. Add a tag `vMAJOR.MINOR.PATCH`. 47 | 3. Run the `publish-spm` workflow on GitHub from the `bdk-swift` repo for version `MAJOR.MINOR.PATCH`. 48 | 49 | [Swift]: https://developer.apple.com/swift/ 50 | [Xcode]: https://developer.apple.com/documentation/Xcode 51 | [`BDK`]: https://github.com/bitcoindevkit/ 52 | [`bdk-ffi`]: https://github.com/bitcoindevkit/bdk-ffi 53 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveElectrumClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | private let SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602" 5 | 6 | final class LiveElectrumClientTests: XCTestCase { 7 | private let descriptor = try! Descriptor( 8 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 9 | network: Network.signet 10 | ) 11 | private let changeDescriptor = try! Descriptor( 12 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 13 | network: Network.signet 14 | ) 15 | 16 | func testSyncedBalance() throws { 17 | let persister = try Persister.newInMemory() 18 | let wallet = try Wallet( 19 | descriptor: descriptor, 20 | changeDescriptor: changeDescriptor, 21 | network: Network.signet, 22 | persister: persister 23 | ) 24 | let electrumClient: ElectrumClient = try ElectrumClient(url: SIGNET_ELECTRUM_URL) 25 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 26 | let update = try electrumClient.fullScan( 27 | request: fullScanRequest, 28 | stopGap: 10, 29 | batchSize: 10, 30 | fetchPrevTxouts: false 31 | ) 32 | try wallet.applyUpdate(update: update) 33 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address 34 | 35 | XCTAssertGreaterThan( 36 | wallet.balance().total.toSat(), 37 | UInt64(0), 38 | "Wallet must have positive balance, please send funds to \(address)" 39 | ) 40 | 41 | print("Transactions count: \(wallet.transactions().count)") 42 | let transactions = wallet.transactions().prefix(3) 43 | for tx in transactions { 44 | let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction) 45 | print("Transaction: \(tx.transaction.computeTxid())") 46 | print("Sent \(sentAndReceived.sent.toSat())") 47 | print("Received \(sentAndReceived.received.toSat())") 48 | } 49 | } 50 | 51 | func testServerFeatures() throws { 52 | let electrumClient: ElectrumClient = try ElectrumClient(url: "ssl://electrum.blockstream.info:60002") 53 | let features: ServerFeaturesRes = try electrumClient.serverFeatures() 54 | print("Server Features:\n\(features)") 55 | 56 | XCTAssertEqual( 57 | features.genesisHash.description, 58 | "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | final class LiveKyotoTests: XCTestCase { 5 | private let descriptor = try! Descriptor( 6 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 7 | network: Network.signet 8 | ) 9 | private let changeDescriptor = try! Descriptor( 10 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 11 | network: Network.signet 12 | ) 13 | private let peer = IpAddress.fromIpv4(q1: 68, q2: 47, q3: 229, q4: 218) 14 | private let cwd = FileManager.default.currentDirectoryPath.appending("/temp") 15 | 16 | override func tearDownWithError() throws { 17 | let fileManager = FileManager.default 18 | if fileManager.fileExists(atPath: cwd) { 19 | try fileManager.removeItem(atPath: cwd) 20 | } 21 | } 22 | 23 | func testSuccessfullySyncs() async throws { 24 | let persister = try Persister.newInMemory() 25 | let wallet = try Wallet( 26 | descriptor: descriptor, 27 | changeDescriptor: changeDescriptor, 28 | network: Network.signet, 29 | persister: persister 30 | ) 31 | let trustedPeer = Peer(address: peer, port: nil, v2Transport: false) 32 | let lightClient = try CbfBuilder() 33 | .peers(peers: [trustedPeer]) 34 | .connections(connections: 1) 35 | .scanType(scanType: ScanType.new) 36 | .dataDir(dataDir: cwd) 37 | .build(wallet: wallet) 38 | let client = lightClient.client 39 | let node = lightClient.node 40 | node.run() 41 | Task { 42 | while true { 43 | if let log = try? await client.nextLog() { 44 | print("\(log)") 45 | } 46 | } 47 | } 48 | let update = try await client.update() 49 | try wallet.applyUpdate(update: update) 50 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 51 | XCTAssertGreaterThan( 52 | wallet.balance().total.toSat(), 53 | UInt64(0), 54 | "Wallet must have positive balance, please send funds to \(address)" 55 | ) 56 | print("Update applied correctly") 57 | try client.shutdown() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveMemoryWalletTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 5 | private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 6 | 7 | final class LiveMemoryWalletTests: XCTestCase { 8 | private let descriptor = try! Descriptor( 9 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 10 | network: Network.signet 11 | ) 12 | private let changeDescriptor = try! Descriptor( 13 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 14 | network: Network.signet 15 | ) 16 | 17 | func testSyncedBalance() throws { 18 | let persister = try Persister.newInMemory() 19 | let wallet = try Wallet( 20 | descriptor: descriptor, 21 | changeDescriptor: changeDescriptor, 22 | network: .signet, 23 | persister: persister 24 | ) 25 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 26 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 27 | let update = try esploraClient.fullScan( 28 | request: fullScanRequest, 29 | stopGap: 10, 30 | parallelRequests: 1 31 | ) 32 | try wallet.applyUpdate(update: update) 33 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 34 | 35 | XCTAssertGreaterThan( 36 | wallet.balance().total.toSat(), 37 | UInt64(0), 38 | "Wallet must have positive balance, please send funds to \(address)" 39 | ) 40 | 41 | print("Transactions count: \(wallet.transactions().count)") 42 | let transactions = wallet.transactions().prefix(3) 43 | for tx in transactions { 44 | let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction) 45 | print("Transaction: \(tx.transaction.computeTxid())") 46 | print("Sent \(sentAndReceived.sent.toSat())") 47 | print("Received \(sentAndReceived.received.toSat())") 48 | } 49 | } 50 | 51 | func testScriptInspector() throws { 52 | let persister = try Persister.newInMemory() 53 | let wallet = try Wallet( 54 | descriptor: descriptor, 55 | changeDescriptor: changeDescriptor, 56 | network: .signet, 57 | persister: persister 58 | ) 59 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 60 | let scriptInspector = FullScriptInspector() 61 | let fullScanRequest = try wallet.startFullScan().inspectSpksForAllKeychains(inspector: scriptInspector).build() 62 | 63 | let update = try esploraClient.fullScan( 64 | request: fullScanRequest, 65 | stopGap: 21, 66 | parallelRequests: 1 67 | ) 68 | try wallet.applyUpdate(update: update) 69 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 70 | 71 | XCTAssertGreaterThan( 72 | wallet.balance().total.toSat(), 73 | UInt64(0), 74 | "Wallet must have positive balance, please send funds to \(address)" 75 | ) 76 | } 77 | 78 | } 79 | 80 | final class FullScriptInspector: FullScanScriptInspector { 81 | func inspect(keychain: KeychainKind, index: UInt32, script: Script) { 82 | print("Inspecting index \(index) for keychain \(keychain)") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveTransactionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 5 | private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 6 | 7 | 8 | final class LiveTransactionTests: XCTestCase { 9 | private let descriptor = try! Descriptor( 10 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 11 | network: Network.signet 12 | ) 13 | private let changeDescriptor = try! Descriptor( 14 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 15 | network: Network.signet 16 | ) 17 | 18 | func testSyncedBalance() throws { 19 | let persister = try Persister.newInMemory() 20 | let wallet = try Wallet( 21 | descriptor: descriptor, 22 | changeDescriptor: changeDescriptor, 23 | network: .signet, 24 | persister: persister 25 | ) 26 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 27 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 28 | let update = try esploraClient.fullScan( 29 | request: fullScanRequest, 30 | stopGap: 10, 31 | parallelRequests: 1 32 | ) 33 | try wallet.applyUpdate(update: update) 34 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 35 | 36 | XCTAssertGreaterThan( 37 | wallet.balance().total.toSat(), 38 | UInt64(0), 39 | "Wallet must have positive balance, please send funds to \(address)" 40 | ) 41 | 42 | guard let transaction = wallet.transactions().first?.transaction else { 43 | print("No transactions found") 44 | return 45 | } 46 | print("First transaction:") 47 | print("Txid: \(transaction.computeTxid())") 48 | print("Version: \(transaction.version())") 49 | print("Total size: \(transaction.totalSize())") 50 | print("Vsize: \(transaction.vsize())") 51 | print("Weight: \(transaction.weight())") 52 | print("Coinbase transaction: \(transaction.isCoinbase())") 53 | print("Is explicitly RBF: \(transaction.isExplicitlyRbf())") 54 | print("Inputs: \(transaction.input())") 55 | print("Outputs: \(transaction.output())") 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 5 | private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 6 | 7 | final class LiveTxBuilderTests: XCTestCase { 8 | private let descriptor = try! Descriptor( 9 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 10 | network: Network.signet 11 | ) 12 | private let changeDescriptor = try! Descriptor( 13 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 14 | network: Network.signet 15 | ) 16 | var dbFilePath: URL! 17 | 18 | override func setUpWithError() throws { 19 | super.setUp() 20 | let fileManager = FileManager.default 21 | let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! 22 | let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).sqlite" 23 | dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName) 24 | 25 | if fileManager.fileExists(atPath: dbFilePath.path) { 26 | try fileManager.removeItem(at: dbFilePath) 27 | } 28 | } 29 | 30 | override func tearDownWithError() throws { 31 | let fileManager = FileManager.default 32 | if fileManager.fileExists(atPath: dbFilePath.path) { 33 | try fileManager.removeItem(at: dbFilePath) 34 | } 35 | } 36 | 37 | func testTxBuilder() throws { 38 | let persister = try Persister.newInMemory() 39 | let wallet = try Wallet( 40 | descriptor: descriptor, 41 | changeDescriptor: changeDescriptor, 42 | network: .signet, 43 | persister: persister 44 | ) 45 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 46 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 47 | let update = try esploraClient.fullScan( 48 | request: fullScanRequest, 49 | stopGap: 10, 50 | parallelRequests: 1 51 | ) 52 | try wallet.applyUpdate(update: update) 53 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 54 | 55 | XCTAssertGreaterThan( 56 | wallet.balance().total.toSat(), 57 | UInt64(0), 58 | "Wallet must have positive balance, please send funds to \(address)" 59 | ) 60 | 61 | let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) 62 | let psbt: Psbt = try TxBuilder() 63 | .addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(satoshi: 4200)) 64 | .feeRate(feeRate: FeeRate.fromSatPerVb(satVb: 2)) 65 | .finish(wallet: wallet) 66 | 67 | print(psbt.serialize()) 68 | XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") 69 | } 70 | 71 | func testComplexTxBuilder() throws { 72 | let persister = try Persister.newInMemory() 73 | let wallet = try Wallet( 74 | descriptor: descriptor, 75 | changeDescriptor: changeDescriptor, 76 | network: .signet, 77 | persister: persister 78 | ) 79 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 80 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 81 | let update = try esploraClient.fullScan( 82 | request: fullScanRequest, 83 | stopGap: 10, 84 | parallelRequests: 1 85 | ) 86 | try wallet.applyUpdate(update: update) 87 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 88 | 89 | XCTAssertGreaterThan( 90 | wallet.balance().total.toSat(), 91 | UInt64(0), 92 | "Wallet must have positive balance, please send funds to \(address)" 93 | ) 94 | 95 | let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) 96 | let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet) 97 | let allRecipients: [ScriptAmount] = [ 98 | ScriptAmount(script: recipient1.scriptPubkey(), amount: Amount.fromSat(satoshi: 4200)), 99 | ScriptAmount(script: recipient2.scriptPubkey(), amount: Amount.fromSat(satoshi: 4200)) 100 | ] 101 | 102 | let psbt: Psbt = try TxBuilder() 103 | .setRecipients(recipients: allRecipients) 104 | .feeRate(feeRate: FeeRate.fromSatPerVb(satVb: 4)) 105 | .finish(wallet: wallet) 106 | 107 | let _ = try! wallet.sign(psbt: psbt) 108 | 109 | XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | import Darwin 4 | 5 | private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" 6 | private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" 7 | 8 | final class LiveWalletTests: XCTestCase { 9 | private let descriptor = try! Descriptor( 10 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 11 | network: Network.signet 12 | ) 13 | private let changeDescriptor = try! Descriptor( 14 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 15 | network: Network.signet 16 | ) 17 | var dbFilePath: URL! 18 | 19 | override func setUpWithError() throws { 20 | super.setUp() 21 | let fileManager = FileManager.default 22 | let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! 23 | let uniqueDbFileName = "bdk_persistence_\(UUID().uuidString).sqlite" 24 | dbFilePath = documentDirectory.appendingPathComponent(uniqueDbFileName) 25 | 26 | if fileManager.fileExists(atPath: dbFilePath.path) { 27 | try fileManager.removeItem(at: dbFilePath) 28 | } 29 | } 30 | 31 | override func tearDownWithError() throws { 32 | let fileManager = FileManager.default 33 | if fileManager.fileExists(atPath: dbFilePath.path) { 34 | try fileManager.removeItem(at: dbFilePath) 35 | } 36 | } 37 | 38 | func testSyncedBalance() throws { 39 | let persister = try Persister.newInMemory() 40 | let wallet = try Wallet( 41 | descriptor: descriptor, 42 | changeDescriptor: changeDescriptor, 43 | network: .signet, 44 | persister: persister 45 | ) 46 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 47 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 48 | let update = try esploraClient.fullScan( 49 | request: fullScanRequest, 50 | stopGap: 10, 51 | parallelRequests: 1 52 | ) 53 | try wallet.applyUpdate(update: update) 54 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 55 | 56 | print("testSyncedBalance - Before balance assertion. Address: \(address)") 57 | print("testSyncedBalance - Before balance assertion. Wallet Balance: \(wallet.balance()))") 58 | print("testSyncedBalance - Before balance assertion. Wallet Balance Total To Sat: \(wallet.balance().total.toSat())") 59 | fflush(stdout) 60 | 61 | XCTAssertGreaterThan( 62 | wallet.balance().total.toSat(), 63 | UInt64(0), 64 | "Wallet must have positive balance, please send funds to \(address)" 65 | ) 66 | 67 | print("Transactions count: \(wallet.transactions().count)") 68 | let transactions = wallet.transactions().prefix(3) 69 | for tx in transactions { 70 | let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction) 71 | print("Transaction: \(tx.transaction.computeTxid())") 72 | print("Sent \(sentAndReceived.sent.toSat())") 73 | print("Received \(sentAndReceived.received.toSat())") 74 | } 75 | } 76 | 77 | func testBroadcastTransaction() throws { 78 | let persister = try Persister.newInMemory() 79 | let wallet = try Wallet( 80 | descriptor: descriptor, 81 | changeDescriptor: changeDescriptor, 82 | network: .signet, 83 | persister: persister 84 | ) 85 | let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) 86 | let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() 87 | let update = try esploraClient.fullScan( 88 | request: fullScanRequest, 89 | stopGap: 10, 90 | parallelRequests: 1 91 | ) 92 | try wallet.applyUpdate(update: update) 93 | let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description 94 | 95 | print("testBroadcastTransaction - Before balance assertion. Address: \(address)") 96 | print("testBroadcastTransaction - Before balance assertion. Wallet Balance: \(wallet.balance()))") 97 | print("testBroadcastTransaction - Before balance assertion. Wallet Balance Total To Sat: \(wallet.balance().total.toSat())") 98 | fflush(stdout) 99 | XCTAssertGreaterThan( 100 | wallet.balance().total.toSat(), 101 | UInt64(0), 102 | "Wallet must have positive balance, please send funds to \(address)" 103 | ) 104 | 105 | print("Balance: \(wallet.balance().total)") 106 | 107 | let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) 108 | let psbt: Psbt = try 109 | TxBuilder() 110 | .addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(satoshi: 4200)) 111 | .feeRate(feeRate: FeeRate.fromSatPerVb(satVb: 2)) 112 | .finish(wallet: wallet) 113 | 114 | print(psbt.serialize()) 115 | XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") 116 | 117 | let walletDidSign: Bool = try wallet.sign(psbt: psbt) 118 | XCTAssertTrue(walletDidSign, "Wallet did not sign transaction") 119 | 120 | let tx: Transaction = try! psbt.extractTx() 121 | print(tx.computeTxid()) 122 | let fee: Amount = try wallet.calculateFee(tx: tx) 123 | print("Transaction Fee: \(fee.toSat())") 124 | let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx) 125 | print("Transaction Fee Rate: \(feeRate.toSatPerVbCeil()) sat/vB") 126 | 127 | try esploraClient.broadcast(transaction: tx) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/OfflineDescriptorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | final class OfflineDescriptorTests: XCTestCase { 5 | func testDescriptorBip86() throws { 6 | let mnemonic: Mnemonic = try Mnemonic.fromString(mnemonic: "space echo position wrist orient erupt relief museum myself grain wisdom tumble") 7 | let descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey( 8 | network: Network.testnet, 9 | mnemonic: mnemonic, 10 | password: nil 11 | ) 12 | let descriptor: Descriptor = Descriptor.newBip86( 13 | secretKey: descriptorSecretKey, 14 | keychainKind: KeychainKind.external, 15 | network: Network.testnet 16 | ) 17 | 18 | XCTAssertEqual(descriptor.description, "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/OfflinePersistenceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | final class OfflinePersistenceTests: XCTestCase { 5 | private let descriptor = try! Descriptor( 6 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", 7 | network: Network.signet 8 | ) 9 | private let changeDescriptor = try! Descriptor( 10 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", 11 | network: Network.signet 12 | ) 13 | var dbFilePath: URL! 14 | 15 | override func setUpWithError() throws { 16 | super.setUp() 17 | 18 | guard let resourceUrl = Bundle.module.url( 19 | forResource: "pre_existing_wallet_persistence_test", 20 | withExtension: "sqlite" 21 | ) else { 22 | print("error finding resourceURL") 23 | return 24 | } 25 | dbFilePath = resourceUrl 26 | } 27 | 28 | func testPersistence() throws { 29 | let persister = try Persister.newSqlite(path: dbFilePath.path) 30 | let wallet = try Wallet.load( 31 | descriptor: descriptor, 32 | changeDescriptor: changeDescriptor, 33 | persister: persister 34 | ) 35 | let nextAddress: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) 36 | print("Address: \(nextAddress)") 37 | 38 | XCTAssertTrue(nextAddress.address.description == "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2") 39 | XCTAssertTrue(nextAddress.index == 7) 40 | } 41 | 42 | func testPersistenceWithDescriptor() throws { 43 | let persister = try Persister.newSqlite(path: dbFilePath.path) 44 | 45 | let descriptorPub = try Descriptor( 46 | descriptor: "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/0/*)#zpaanzgu", 47 | network: Network.signet 48 | ) 49 | let changeDescriptorPub = try Descriptor( 50 | descriptor: "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/1/*)#n4cuwhcy", 51 | network: Network.signet 52 | ) 53 | 54 | let wallet = try Wallet.load( 55 | descriptor: descriptorPub, 56 | changeDescriptor: changeDescriptorPub, 57 | persister: persister 58 | ) 59 | let nextAddress: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) 60 | print("Address: \(nextAddress)") 61 | 62 | XCTAssertEqual(nextAddress.index, 7) 63 | XCTAssertEqual(nextAddress.address.description, "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BitcoinDevKit 3 | 4 | final class OfflineWalletTests: XCTestCase { 5 | private let descriptor = try! Descriptor( 6 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", 7 | network: Network.signet 8 | ) 9 | private let changeDescriptor = try! Descriptor( 10 | descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/1/*)", 11 | network: Network.signet 12 | ) 13 | 14 | func testNewAddress() throws { 15 | let persister = try Persister.newInMemory() 16 | let wallet = try Wallet( 17 | descriptor: descriptor, 18 | changeDescriptor: changeDescriptor, 19 | network: .signet, 20 | persister: persister 21 | ) 22 | let addressInfo: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) 23 | 24 | XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet), 25 | "Address is not valid for testnet network") 26 | XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.signet), 27 | "Address is not valid for signet network") 28 | XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.regtest), 29 | "Address is valid for regtest network, but it shouldn't be") 30 | XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin), 31 | "Address is valid for bitcoin network, but it shouldn't be") 32 | 33 | XCTAssertEqual(addressInfo.address.description, "tb1qhjys9wxlfykmte7ftryptx975uqgd6kcm6a7z4") 34 | } 35 | 36 | func testBalance() throws { 37 | let persister = try Persister.newInMemory() 38 | let wallet = try Wallet( 39 | descriptor: descriptor, 40 | changeDescriptor: changeDescriptor, 41 | network: .signet, 42 | persister: persister 43 | ) 44 | 45 | XCTAssertEqual(wallet.balance().total.toSat(), 0) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bdk-swift/Tests/BitcoinDevKitTests/Resources/pre_existing_wallet_persistence_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoindevkit/bdk-ffi/f1984dffc9bf1c9185bd16eecef2204393a6e920/bdk-swift/Tests/BitcoinDevKitTests/Resources/pre_existing_wallet_persistence_test.sqlite -------------------------------------------------------------------------------- /bdk-swift/build-xcframework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script builds local swift-bdk Swift language bindings and corresponding bdkFFI.xcframework. 3 | # The results of this script can be used for locally testing your SPM package adding a local package 4 | # to your application pointing at the bdk-swift directory. 5 | 6 | HEADERPATH="Sources/BitcoinDevKit/BitcoinDevKitFFI.h" 7 | MODMAPPATH="Sources/BitcoinDevKit/BitcoinDevKitFFI.modulemap" 8 | TARGETDIR="../bdk-ffi/target" 9 | OUTDIR="." 10 | RELDIR="release-smaller" 11 | NAME="bdkffi" 12 | STATIC_LIB_NAME="lib${NAME}.a" 13 | NEW_HEADER_DIR="../bdk-ffi/target/include" 14 | 15 | # set required rust version and install component and targets 16 | rustup default 1.84.1 17 | rustup component add rust-src 18 | rustup target add aarch64-apple-ios # iOS arm64 19 | rustup target add x86_64-apple-ios # iOS x86_64 20 | rustup target add aarch64-apple-ios-sim # simulator mac M1 21 | rustup target add aarch64-apple-darwin # mac M1 22 | rustup target add x86_64-apple-darwin # mac x86_64 23 | 24 | cd ../bdk-ffi/ || exit 25 | 26 | # build bdk-ffi rust lib for apple targets 27 | cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-darwin 28 | cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-darwin 29 | cargo build --package bdk-ffi --profile release-smaller --target x86_64-apple-ios 30 | cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios 31 | cargo build --package bdk-ffi --profile release-smaller --target aarch64-apple-ios-sim 32 | 33 | # build bdk-ffi Swift bindings and put in bdk-swift Sources 34 | 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 35 | 36 | # combine bdk-ffi static libs for aarch64 and x86_64 targets via lipo tool 37 | mkdir -p target/lipo-ios-sim/release-smaller 38 | 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 39 | mkdir -p target/lipo-macos/release-smaller 40 | 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 41 | 42 | cd ../bdk-swift/ || exit 43 | 44 | # move bdk-ffi static lib header files to temporary directory 45 | mkdir -p "${NEW_HEADER_DIR}" 46 | mv "${HEADERPATH}" "${NEW_HEADER_DIR}" 47 | mv "${MODMAPPATH}" "${NEW_HEADER_DIR}/module.modulemap" 48 | echo -e "\n" >> "${NEW_HEADER_DIR}/module.modulemap" 49 | 50 | # remove old xcframework directory 51 | rm -rf "${OUTDIR}/${NAME}.xcframework" 52 | 53 | # create new xcframework directory from bdk-ffi static libs and headers 54 | xcodebuild -create-xcframework \ 55 | -library "${TARGETDIR}/lipo-macos/${RELDIR}/${STATIC_LIB_NAME}" \ 56 | -headers "${NEW_HEADER_DIR}" \ 57 | -library "${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" \ 58 | -headers "${NEW_HEADER_DIR}" \ 59 | -library "${TARGETDIR}/lipo-ios-sim/${RELDIR}/${STATIC_LIB_NAME}" \ 60 | -headers "${NEW_HEADER_DIR}" \ 61 | -output "${OUTDIR}/${NAME}.xcframework" 62 | -------------------------------------------------------------------------------- /bdk-swift/justfile: -------------------------------------------------------------------------------- 1 | [group("Repo")] 2 | [doc("Default command; list all available commands.")] 3 | @list: 4 | just --list --unsorted 5 | 6 | [group("Repo")] 7 | [doc("Open repo on GitHub in your default browser.")] 8 | repo: 9 | open https://github.com/bitcoindevkit/bdk-ffi 10 | 11 | [group("Build")] 12 | [doc("Build the library.")] 13 | build: 14 | bash ./build-xcframework.sh 15 | 16 | [group("Build")] 17 | [doc("Remove all caches and previous builds to start from scratch.")] 18 | clean: 19 | rm -rf ../bdk-ffi/target/ 20 | 21 | [group("Test")] 22 | [doc("Run all tests, unless a filter is provided.")] 23 | test *FILTER: 24 | swift test {{ if FILTER == "" { "" } else { "--filter " + FILTER } }} 25 | 26 | [group("Test")] 27 | [doc("Run only offline tests.")] 28 | test-offline: 29 | swift test --filter Offline 30 | -------------------------------------------------------------------------------- /docs/adr/01-naming.md: -------------------------------------------------------------------------------- 1 | # Naming convention 2 | 3 | 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. 4 | 5 | ## Context and Problem Statement 6 | 7 | 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. 8 | 9 | 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). 10 | 11 | ## Decision Drivers 12 | 13 | Our main goals are: 14 | 1. Keep the multiple language bindings libraries maintainable. 15 | 2. Help users of bdk help each other and working with a similarly shaped API across languages. 16 | 17 | ## Decision Outcome 18 | 19 | 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. 20 | 21 | 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. 22 | 23 | [uniffi]: https://github.com/mozilla/uniffi-rs/ 24 | -------------------------------------------------------------------------------- /docs/adr/02-wrapping.md: -------------------------------------------------------------------------------- 1 | # Wrapping BDK APIs 2 | 3 | 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. 4 | 5 | ## Context and Problem Statement 6 | 7 | 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. 8 | 9 | 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). 10 | 11 | ## Decision Drivers 12 | 13 | Our main goals are: 14 | 1. Keep the multiple language bindings libraries maintainable. 15 | 2. Help users of bdk help each other and working with a similarly shaped API across languages. 16 | 17 | ## Decision Outcome 18 | 19 | There are three potential reasons for wrapping Rust BDK APIs: 20 | 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) 21 | 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 22 | 3. Some extra feature/utility might be interesting for our end-users 23 | 24 | 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). 25 | 26 | [uniffi]: https://github.com/mozilla/uniffi-rs/ 27 | -------------------------------------------------------------------------------- /docs/adr/03-errrors.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 | Returning meaningful errors is important for users of our libraries. Libraries return errors and applications decide if and how those errors are formatted and displayed to users. Our goal as a library is to produce meaningful and structured error types which allows applications to easily differentiate various error cases. The Rust bitcoin ecosystem uses descriptive and data-driven errors, and we would like to stay as close to them as possible when building language bindings for these libraries. 4 | 5 | ## Context and Problem Statement 6 | 7 | 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. Those come into play when attempting to expose Rust errors; we must choose between simple enums that have variants but not data associated with them (for example, a `TxidError` would not be able to return the specific txid that triggered the error), or more complex objects that do have data fields, but no ability to control the error message returned to the user. What's more, while Kotlin and Java users expect a `message` field on an `Exception` type, this field becomes an empty string if the object has no data, and just a strignified version of the fields if it does have any. Swift, in contrast, can optionally leverage something like the `LocalizedError` protocol to provide custom and localized descriptions. 8 | 9 | ## Decision Drivers 10 | 11 | Some options were considered and explored in detail in [issue #509]. Some aspects of this decision include: 12 | - Expectations from devs using the libraries in different languages (for example, Kotlin users expect a `message` field on the exception, whereas Swift user expect a `localizedDescription`) 13 | - Ease of maintaining the errors as they evolve and we expose more and more of the Rust BDK ecosystem 14 | - Important vs nice-to-have features (some fields can be stringified without loss of information, some cannot) 15 | 16 | ## Decision Outcome 17 | 18 | We have decided to leverage two different approaches that uniffi offers for exposing errors, and using the most appropriate one for each situation. 19 | 20 | In the case where errors do not require passing back data, we opt for the simpler to maintain option, which provides better, customized error message with a `message` field provided by the `thiserror` library. Those errors cannot have fields. For example: 21 | 22 | ### UDL 23 | 24 | ```txt 25 | [Error] 26 | enum FeeRateError { 27 | "ArithmeticOverflow" 28 | }; 29 | ``` 30 | 31 | #### Rust 32 | ```rust 33 | #[derive(Debug, thiserror::Error)] 34 | pub enum FeeRateError { 35 | #[error("arithmetic overflow on feerate {fee_rate}")] 36 | ArithmeticOverflow { fee_rate: u64 }, 37 | } 38 | ``` 39 | 40 | In the case where complex types should be returned as part of the error, we use the more complex interface UDL type and return the data, at the cost of more explicit messages. For example: 41 | 42 | ### UDL 43 | 44 | ```txt 45 | [Error] 46 | interface CalculateFeeError { 47 | MissingTxOut(sequence out_points); 48 | }; 49 | ``` 50 | 51 | ### Rust 52 | 53 | ```rust 54 | #[derive(Debug, thiserror::Error)] 55 | pub enum CalculateFeeError { 56 | #[error("missing transaction output: {out_points:?}")] 57 | MissingTxOut { out_points: Vec }, 58 | } 59 | ``` 60 | 61 | [uniffi]: https://github.com/mozilla/uniffi-rs/ 62 | [issue #509]: https://github.com/bitcoindevkit/bdk-ffi/issues/509 63 | -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | # Architectural Decision Records 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | Some more readings on ADRs: 8 | - https://www.ozimmer.ch/practices/2023/04/03/ADRCreation.html 9 | - https://github.com/joelparkerhenderson/architecture-decision-record 10 | - https://adr.github.io/ 11 | - https://betterprogramming.pub/the-ultimate-guide-to-architectural-decision-records-6d74fd3850ee 12 | - https://www.redhat.com/architect/architecture-decision-records 13 | --------------------------------------------------------------------------------