diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..2c89533 Binary files /dev/null and b/.DS_Store differ diff --git a/bdk-python/.github/ISSUE_TEMPLATE/bug_report.md b/bdk-python/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3d2de18 --- /dev/null +++ b/bdk-python/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: 🐞 Bug +about: File a bug/issue +title: '[BUG:] ' +labels: bug +--- + +<!-- +Note: Please search to see if an issue already exists for the bug you encountered. +--> + +### Current Behavior: +<!-- A concise description of what you're experiencing. --> + +### Expected Behavior: +<!-- A concise description of what you expected to happen. --> + +### Steps To Reproduce: +<!-- +Example: steps to reproduce the behavior: +1. In this environment... +2. With this config... +3. Run '...' +4. See error... +--> + +### Environment: +<!-- +Example: +- OS: Ubuntu 20.04 +- Node: 13.14.0 +- npm: 7.6.3 +--> + +### Anything else: +<!-- +Links? References? Anything that will give us more context about the issue that you are encountering! +--> diff --git a/bdk-python/.github/ISSUE_TEMPLATE/feature_request.md b/bdk-python/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6fd56fd --- /dev/null +++ b/bdk-python/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: 🚀 Feature Request +about: Request a feature +title: '[Feature Request:] <title>' +labels: enhancement +--- + +### Is your proposal related to a problem? +<!-- + Provide a clear and concise description of what the problem is. + For example, "I'm always frustrated when..." +--> + +### Describe the solution you'd like +<!-- + Provide a clear and concise description of what you want to happen. +--> + +### Additional context +<!-- + Is there anything else you can add about the proposal? + You might want to link to related issues here, if you haven't already. +--> diff --git a/bdk-python/.github/pull_request_template.md b/bdk-python/.github/pull_request_template.md new file mode 100644 index 0000000..c44596a --- /dev/null +++ b/bdk-python/.github/pull_request_template.md @@ -0,0 +1,24 @@ +<!-- Erase any parts of this template not applicable to your Pull Request. --> + +## Description +<!-- Describe the purpose of this PR, what's being adding and/or fixed --> + +## Notes to the reviewers +<!-- In this section you can include notes directed to the reviewers, like explaining why some parts +of the PR were done in a specific way --> + +## Checklists + +## All Submissions: +* [ ] I've signed all my commits +* [ ] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) + +#### New Features: +* [ ] I've added tests for the new feature +* [ ] I've added docs for the new feature +* [ ] I've updated `CHANGELOG.md` + +#### Bugfixes: +* [ ] This pull request breaks the existing API +* [ ] I've added tests to reproduce the issue which are now passing +* [ ] I'm linking the issue being fixed by this PR diff --git a/bdk-python/.github/workflows/build.yml b/bdk-python/.github/workflows/build.yml new file mode 100644 index 0000000..e4e8293 --- /dev/null +++ b/bdk-python/.github/workflows/build.yml @@ -0,0 +1,109 @@ +name: Build wheels +on: [push, pull_request] + +# We use manylinux2014 because older CentOS versions used by 2010 and 1 have a very old glibc version, which +# makes it very hard to use GitHub's javascript actions (checkout, upload-artifact, etc). +# They mount their own nodejs interpreter inside your container, but since that's not statically linked it +# tries to load glibc and fails because it requires a more recent version. + +jobs: + build-manylinux2014-x86_64-wheel: + name: 'Build Manylinux 2014 x86_64 wheel' + runs-on: ubuntu-latest + container: + image: quay.io/pypa/manylinux2014_x86_64 + env: + PLAT: manylinux2014_x86_64 + PYBIN: '/opt/python/${{ matrix.python }}/bin' + strategy: + matrix: + python: # Update this list whenever the docker image is updated (check /opt/python/) + - cp36-cp36m + - cp37-cp37m + - cp38-cp38 + - cp39-cp39 + - cp310-cp310 + - pp37-pypy37_pp73 + - pp38-pypy38_pp73 + steps: + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: install requirements + run: ${PYBIN}/pip install -r requirements.txt + - name: generate bindings + run: bash generate.sh + - name: build wheel + run: ${PYBIN}/pip wheel . --no-deps -w /tmp/wheelhouse + - name: repair wheel + run: auditwheel repair /tmp/wheelhouse/* --plat "$PLAT" -w /tmp/wheelhouse-repaired + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-manylinux2014-x86_64-${{ matrix.python }} + path: /tmp/wheelhouse-repaired/*.whl + + build-macos-universal-wheel: + name: 'Build macOS universal wheel' + runs-on: macos-latest + strategy: + matrix: + python: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: python3 --version + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - run: rustup target add aarch64-apple-darwin + - run: pip3 install --user -r requirements.txt + - run: pip3 install --user wheel + - run: bash generate.sh + - name: build wheel + env: + ARCHFLAGS: "-arch x86_64 -arch arm64" + run: python3 setup.py -v bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-macos-${{ matrix.python }} + path: dist/*.whl + + build-windows-wheel: + name: 'Build windows wheel' + runs-on: windows-latest + strategy: + matrix: + python: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: python --version + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - run: pip install --user -r requirements.txt + - run: ./generate.sh + shell: bash + - run: pip install --user wheel + - name: build wheel + run: python setup.py -v bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-win-${{ matrix.python }} + path: dist/*.whl diff --git a/bdk-python/.github/workflows/publish.yml b/bdk-python/.github/workflows/publish.yml new file mode 100644 index 0000000..aeb16e6 --- /dev/null +++ b/bdk-python/.github/workflows/publish.yml @@ -0,0 +1,141 @@ +name: Build and publish wheels on PyPI +on: [workflow_dispatch] + +# We use manylinux2014 because older CentOS versions used by 2010 and 1 have a very old glibc version, which +# makes it very hard to use GitHub's javascript actions (checkout, upload-artifact, etc). +# They mount their own nodejs interpreter inside your container, but since that's not statically linked it +# tries to load glibc and fails because it requires a more recent version. + +jobs: + build-manylinux2014-x86_64-wheel: + name: 'Build Manylinux 2014 x86_64 wheel' + runs-on: ubuntu-latest + container: + image: quay.io/pypa/manylinux2014_x86_64 + env: + PLAT: manylinux2014_x86_64 + PYBIN: '/opt/python/${{ matrix.python }}/bin' + strategy: + matrix: + python: # Update this list whenever the docker image is updated (check /opt/python/) + - cp36-cp36m + - cp37-cp37m + - cp38-cp38 + - cp39-cp39 + - cp310-cp310 + - pp37-pypy37_pp73 + - pp38-pypy38_pp73 + steps: + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: install requirements + run: ${PYBIN}/pip install -r requirements.txt + - name: generate bindings + run: bash generate.sh + - name: build wheel + run: ${PYBIN}/pip wheel . --no-deps -w /tmp/wheelhouse + - name: repair wheel + run: auditwheel repair /tmp/wheelhouse/* --plat "$PLAT" -w /tmp/wheelhouse-repaired + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-manylinux2014-x86_64-${{ matrix.python }} + path: /tmp/wheelhouse-repaired/*.whl + + build-macos-universal-wheel: + name: 'Build macOS universal wheel' + runs-on: macos-latest + strategy: + matrix: + python: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: python3 --version + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - run: rustup target add aarch64-apple-darwin + - run: pip3 install --user -r requirements.txt + - run: pip3 install --user wheel + - run: bash generate.sh + - name: build wheel + env: + ARCHFLAGS: "-arch x86_64 -arch arm64" + run: python3 setup.py -v bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-macos-${{ matrix.python }} + path: dist/*.whl + + build-windows-wheel: + name: 'Build windows wheel' + runs-on: windows-latest + strategy: + matrix: + python: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: python --version + - name: checkout + uses: actions/checkout@v2 + with: + submodules: true + - run: pip install --user -r requirements.txt + - run: ./generate.sh + shell: bash + - run: pip install --user wheel + - name: build wheel + run: python setup.py -v bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: bdkpython-win-${{ matrix.python }} + path: dist/*.whl + + publish-pypi: + name: 'Publish on PyPI' + runs-on: ubuntu-latest + needs: [build-manylinux2014-x86_64-wheel, build-macos-universal-wheel, build-windows-wheel] + # needs: [build-macos-universal-wheel] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: 'Download artifacts in dist/ directory' + uses: actions/download-artifact@v2 + with: + path: dist/ + + # - name: Display structure of downloaded files + # run: ls -R + + # - name: 'Publish on test PyPI' + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # user: __token__ + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository_url: https://test.pypi.org/legacy/ + # packages_dir: dist/*/ + + - name: 'Publish on PyPI' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: dist/*/ \ No newline at end of file diff --git a/bdk-python/.gitignore b/bdk-python/.gitignore new file mode 100644 index 0000000..726b07a --- /dev/null +++ b/bdk-python/.gitignore @@ -0,0 +1,16 @@ +.tox/ +dist/ +bdkpython.egg-info/ +__pycache__/ +libbdkffi.dylib +.idea/ +.DS_Store + +*.swp + +src/bdkpython/bdk.py +src/bdkpython/*.so +*.whl +build/ + +testing-setup-py-simple-example.py diff --git a/bdk-python/.gitmodules b/bdk-python/.gitmodules new file mode 100644 index 0000000..c582725 --- /dev/null +++ b/bdk-python/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bdk-ffi"] + path = bdk-ffi + url = https://github.com/bitcoindevkit/bdk-ffi.git diff --git a/bdk-python/LICENSE b/bdk-python/LICENSE new file mode 100644 index 0000000..c3f44ca --- /dev/null +++ b/bdk-python/LICENSE @@ -0,0 +1,14 @@ +This software is licensed under [Apache 2.0](LICENSE-APACHE) or +[MIT](LICENSE-MIT), at your option. + +Some files retain their own copyright notice, however, for full authorship +information, see version control history. + +Except as otherwise noted in individual files, all files in this repository are +licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or +http://opensource.org/licenses/MIT>, at your option. + +You may not use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of this software or any files in this repository except in +accordance with one or both of these licenses. diff --git a/bdk-python/LICENSE-APACHE b/bdk-python/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/bdk-python/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bdk-python/LICENSE-MIT b/bdk-python/LICENSE-MIT new file mode 100644 index 0000000..0e0676a --- /dev/null +++ b/bdk-python/LICENSE-MIT @@ -0,0 +1,17 @@ + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bdk-python/README.md b/bdk-python/README.md new file mode 100644 index 0000000..c89f245 --- /dev/null +++ b/bdk-python/README.md @@ -0,0 +1,53 @@ +# bdk-python +The Python language bindings for the [bitcoindevkit](https://github.com/bitcoindevkit). + +See the [package on PyPI](https://pypi.org/project/bdkpython/). +<br/> + +## Install from PyPI +Install the latest release using +```shell +pip install bdkpython +``` +<br/> + +## Run the tests +```shell +pip3 install --requirement requirements.txt +bash ./generate.sh +python3 setup.py --verbose bdist_wheel +pip3 install ./dist/bdkpython-<yourversion>-py3-none-any.whl +python -m unittest --verbose tests/test_bdk.py +``` +<br/> + +## Build the package +```shell +# Install dependencies +pip install --requirement requirements.txt + +# Generate the bindings first +bash generate.sh + +# Build the wheel +python3 setup.py --verbose bdist_wheel +``` +<br/> + +## Run tox to build and test locally +```shell +# install dev requirements +pip install --requirement requirements-dev.txt + +# build bindings glue code (located at ./src/bdkpython/bdk.py) +source ./generate.sh + +# build and test +tox -vv +``` +<br/> + +## Install locally +```shell +pip install ./dist/bdkpython-<yourversion>-py3-none-any.whl +``` diff --git a/bdk-python/bdk-ffi/.editorconfig b/bdk-python/bdk-ffi/.editorconfig new file mode 100644 index 0000000..4f1c5cd --- /dev/null +++ b/bdk-python/bdk-ffi/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.rs] +indent_size = 4 + +[*.kt] +indent_size = 4 + +[*.gradle] +indent_size = 4 + +[tests/**/*.rs] +charset = utf-8 +end_of_line = unset +indent_size = unset +indent_style = unset +trim_trailing_whitespace = unset +insert_final_newline = unset diff --git a/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/bug_report.md b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0708e29 --- /dev/null +++ b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +<!-- A clear and concise description of what the bug is. --> + +**To Reproduce** +<!-- Steps or code to reproduce the behavior. --> + +**Expected behavior** +<!-- A clear and concise description of what you expected to happen. --> + +**Build environment** + - BDK tag/commit: <!-- e.g. v0.13.0, 3a07614 --> + - OS+version: <!-- e.g. ubuntu 20.04.01, macOS 12.0.1, windows --> + - Rust/Cargo version: <!-- e.g. 1.56.0 --> + - Rust/Cargo target: <!-- e.g. x86_64-apple-darwin, x86_64-unknown-linux-gnu, etc. --> + +**Additional context** +<!-- Add any other context about the problem here. --> diff --git a/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/minor_release.md b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/minor_release.md new file mode 100644 index 0000000..0514d56 --- /dev/null +++ b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/minor_release.md @@ -0,0 +1,101 @@ +--- +name: Minor Release +about: Create a new minor release [for release managers only] +title: 'Release MAJOR.MINOR+1.0' +labels: 'release' +assignees: '' + +--- + +## Create a new minor release + +### Summary + +<--release summary to be used in announcements--> + +### Commit + +<--latest commit ID to include in this release--> + +### Changelog + +<--add notices from PRs merged since the prior release, see ["keep a changelog"]--> + +### Checklist + +Release numbering must follow [Semantic Versioning]. These steps assume the current `master` +branch **development** version is *MAJOR.MINOR.0*. + +#### On the day of the feature freeze + +Change the `master` branch to the next MINOR+1 version: + +- [ ] Switch to the `master` branch. +- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR+1`, eg. `bump_dev_0_22`. +- [ ] Bump the `bump_dev_MAJOR_MINOR+1` branch to the next development MINOR+1 version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`. + - The commit message should be "Bump version to MAJOR.MINOR+1.0". +- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR+1` branch to `master`. + - Title PR "Bump version to MAJOR.MINOR+1.0". + +Create a new release branch: + +- [ ] Double check that your local `master` is up-to-date with the upstream repo. +- [ ] Create a new branch called `release/MAJOR.MINOR+1` from `master`. + +Add a release candidate tag, this is optional and only needed for major `bdk-ffi` changes that +require a longer testing cycle: + +- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.1` version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.1`. + - The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.1". +- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch. + - The tag name should be `vMAJOR.MINOR+1.0-rc.1` + - Use message "Release MAJOR.MINOR+1.0 rc.1". + - Make sure the tag is signed, for extra safety use the explicit `--sign` flag. +- [ ] Push the `release/MAJOR.MINOR` branch and new tag to the `bitcoindevkit/bdk` repo. + - Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.1` tag. + +If any issues need to be fixed before the *MAJOR.MINOR+1.0* version is released: + +- [ ] Merge fix PRs to the `master` branch. +- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR+1` branch. +- [ ] Verify fixes in `release/MAJOR.MINOR+1` branch. +- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.x+1` version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.x+1`. + - The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.x+1". +- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch. + - The tag name should be `vMAJOR.MINOR+1.0-rc.x+1`, where x is the current release candidate number. + - Use tag message "Release MAJOR.MINOR+1.0 rc.x+1". + - Make sure the tag is signed, for extra safety use the explicit `--sign` flag. +- [ ] Push the new tag to the `bitcoindevkit/bdk` repo. + - Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.x+1` tag. + +#### On the day of the release + +Tag and publish new release: + +- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0` version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`. + - The commit message should be "Bump version to MAJOR.MINOR+1.0". +- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch. + - The tag name should be `vMAJOR.MINOR+1.0` + - The first line of the tag message should be "Release MAJOR.MINOR+1.0". + - In the body of the tag message put a copy of the **Summary** and **Changelog** for the release. + - Make sure the tag is signed, for extra safety use the explicit `--sign` flag. +- [ ] Wait for the CI to finish one last time. +- [ ] Push the new tag to the `bitcoindevkit/bdk` repo. +- [ ] Create the release on GitHub. + - Go to "tags", click on the dots on the right and select "Create Release". + - Set the title to `Release MAJOR.MINOR+1.0`. + - In the release notes body put the **Summary** and **Changelog**. + - Use the "+ Auto-generate release notes" button to add details from included PRs. + - Until we reach a `1.0.0` release check the "Pre-release" box. +- [ ] After downstream language repos are also updated announce the release, using the **Summary**, + on Discord, Twitter and Mastodon. +- [ ] Celebrate 🎉 + +[Semantic Versioning]: https://semver.org/ +[crates.io]: https://crates.io/crates/bdk +[docs.rs]: https://docs.rs/bdk/latest/bdk +["keep a changelog"]: https://keepachangelog.com/en/1.0.0/ diff --git a/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/patch_release.md b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/patch_release.md new file mode 100644 index 0000000..b292668 --- /dev/null +++ b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/patch_release.md @@ -0,0 +1,69 @@ +--- +name: Patch Release +about: Create a new patch release [for release managers only] +title: 'Release MAJOR.MINOR.PATCH+1' +labels: 'release' +assignees: '' + +--- + +## Create a new patch release + +### Summary + +<--release summary to be used in announcements--> + +### Commit + +<--latest commit ID to include in this release--> + +### Changelog + +<--add notices from PRs merged since the prior release, see ["keep a changelog"]--> + +### Checklist + +Release numbering must follow [Semantic Versioning]. These steps assume the current `master` +branch **development** version is *MAJOR.MINOR.PATCH*. + +### On the day of the patch release + +Change the `master` branch to the new PATCH+1 version: + +- [ ] Switch to the `master` branch. +- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR_PATCH+1`, eg. `bump_dev_0_22_1`. +- [ ] Bump the `bump_dev_MAJOR_MINOR` branch to the next development PATCH+1 version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR.PATCH+1`. + - The commit message should be "Bump version to MAJOR.MINOR.PATCH+1". +- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR_PATCH+1` branch to `master`. + - Title PR "Bump version to MAJOR.MINOR.PATCH+1". + +Cherry-pick, tag and publish new PATCH+1 release: + +- [ ] Merge fix PRs to the `master` branch. +- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR` branch to be patched. +- [ ] Verify fixes in `release/MAJOR.MINOR` branch. +- [ ] Bump the `release/MAJOR.MINOR.PATCH+1` branch to `MAJOR.MINOR.PATCH+1` version. + - Change the `Cargo.toml` version value to `MAJOR.MINOR.MINOR.PATCH+1`. + - The commit message should be "Bump version to MAJOR.MINOR.PATCH+1". +- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR` branch. + - The tag name should be `vMAJOR.MINOR.PATCH+1` + - The first line of the tag message should be "Release MAJOR.MINOR.PATCH+1". + - In the body of the tag message put a copy of the **Summary** and **Changelog** for the release. + - Make sure the tag is signed, for extra safety use the explicit `--sign` flag. +- [ ] Wait for the CI to finish one last time. +- [ ] Push the new tag to the `bitcoindevkit/bdk` repo. +- [ ] Create the release on GitHub. + - Go to "tags", click on the dots on the right and select "Create Release". + - Set the title to `Release MAJOR.MINOR.PATCH+1`. + - In the release notes body put the **Summary** and **Changelog**. + - Use the "+ Auto-generate release notes" button to add details from included PRs. + - Until we reach a `1.0.0` release check the "Pre-release" box. +- [ ] After downstream language repos are also updated announce the release, using the **Summary**, + on Discord, Twitter and Mastodon. +- [ ] Celebrate 🎉 + +[Semantic Versioning]: https://semver.org/ +[crates.io]: https://crates.io/crates/bdk +[docs.rs]: https://docs.rs/bdk/latest/bdk +["keep a changelog"]: https://keepachangelog.com/en/1.0.0/ diff --git a/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/summer_project.md b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/summer_project.md new file mode 100644 index 0000000..a693283 --- /dev/null +++ b/bdk-python/bdk-ffi/.github/ISSUE_TEMPLATE/summer_project.md @@ -0,0 +1,77 @@ +--- +name: Summer of Bitcoin Project +about: Template to suggest a new https://www.summerofbitcoin.org/ project. +title: '' +labels: 'summer-of-bitcoin' +assignees: '' + +--- + +<!-- +## Overview + +Project ideas are scoped for a university-level student with a basic background in CS and bitcoin +fundamentals - achievable over 12-weeks. Below are just a few types of ideas: + + - Low-hanging fruit: Relatively short projects with clear goals; requires basic technical knowledge + and minimal familiarity with the codebase. + - Core development: These projects derive from the ongoing work from the core of your development + team. The list of features and bugs is never-ending, and help is always welcome. + - Risky/Exploratory: These projects push the scope boundaries of your development effort. They + might require expertise in an area not covered by your current development team. They might take + advantage of a new technology. There is a reasonable chance that the project might be less + successful, but the potential rewards make it worth the attempt. + - Infrastructure/Automation: These projects are the code that your organization uses to get its + development work done; for example, projects that improve the automation of releases, regression + tests and automated builds. This is a category where a Summer of Bitcoin student can be really + helpful, doing work that the development team has been putting off while they focus on core + development. + - Quality Assurance/Testing: Projects that work on and test your project's software development + process. Additionally, projects that involve a thorough test and review of individual PRs. + - Fun/Peripheral: These projects might not be related to the current core development focus, but + create new innovations and new perspectives for your project. +--> + +**Description** +<!-- Description: 3-7 sentences describing the project background and tasks to be done. --> + +**Expected Outcomes** +<!-- Short bullet list describing what is to be accomplished --> + +**Resources** +<!-- 2-3 reading materials for candidate to learn about the repo, project, scope etc --> +<!-- Recommended reading such as a developer/contributor guide --> +<!-- [Another example a paper citation](https://arxiv.org/pdf/1802.08091.pdf) --> +<!-- [Another example an existing issue](https://github.com/opencv/opencv/issues/11013) --> +<!-- [An existing related module](https://github.com/opencv/opencv_contrib/tree/master/modules/optflow) --> + +**Skills Required** +<!-- 3-4 technical skills that the candidate should know --> +<!-- hands on experience with git --> +<!-- mastery plus experience coding in C++ --> +<!-- basic knowledge in matrix and tensor computations, college course work in cryptography --> +<!-- strong mathematical background --> +<!-- Bonus - has experience with React Native. Best if you have also worked with OSSFuzz --> + +**Mentor(s)** +<!-- names of mentor(s) for this project go here --> + +**Difficulty** +<!-- Easy, Medium, Hard --> + +**Competency Test (optional)** +<!-- 2-3 technical tasks related to the project idea or repository you’d like a candidate to + perform in order to demonstrate competency, good first bugs, warm-up exercises --> +<!-- ex. Read the instructions here to get Bitcoin core running on your machine --> +<!-- ex. pick an issue labeled as “newcomer” in the repository, and send a merge request to the + repository. You can also suggest some other improvement that we did not think of yet, or + something that you find interesting or useful --> +<!-- ex. fixes for coding style are usually easy to do, and are good issues for first time + contributions for those learning how to interact with the project. After you are done with the + coding style issue, try making a different contribution. --> +<!-- ex. setup a full Debian packaging development environment and learn the basics of Debian + packaging. Then identify and package the missing dependencies to package Specter Desktop --> +<!-- ex. write a pull parser for CSV files. You'll be judged by the decisions to store the parser + state and how flexible it is to wrap this parser in other scenarios. --> +<!-- ex. Stretch Goal: Implement some basic metaprogram/app to prove you're very familiar with BDK. + Be prepared to make adjustments as we judge your solution. --> diff --git a/bdk-python/bdk-ffi/.github/pull_request_template.md b/bdk-python/bdk-ffi/.github/pull_request_template.md new file mode 100644 index 0000000..42afd7e --- /dev/null +++ b/bdk-python/bdk-ffi/.github/pull_request_template.md @@ -0,0 +1,34 @@ +<!-- You can erase any parts of this template not applicable to your Pull Request. --> + +### Description + +<!-- Describe the purpose of this PR, what's being adding and/or fixed --> + +### Notes to the reviewers + +<!-- In this section you can include notes directed to the reviewers, like explaining why some parts +of the PR were done in a specific way --> + +### Changelog notice + +<!-- Notice the release manager should include in the release tag message changelog --> +<!-- See https://keepachangelog.com/en/1.0.0/ for examples --> + +### Checklists + +#### All Submissions: + +* [ ] I've signed all my commits +* [ ] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) +* [ ] I ran `cargo fmt` and `cargo clippy` before committing + +#### New Features: + +* [ ] I've added tests for the new feature +* [ ] I've added docs for the new feature + +#### Bugfixes: + +* [ ] This pull request breaks the existing API +* [ ] I've added tests to reproduce the issue which are now passing +* [ ] I'm linking the issue being fixed by this PR diff --git a/bdk-python/bdk-ffi/.github/workflows/audit.yml b/bdk-python/bdk-ffi/.github/workflows/audit.yml new file mode 100644 index 0000000..6143cca --- /dev/null +++ b/bdk-python/bdk-ffi/.github/workflows/audit.yml @@ -0,0 +1,19 @@ +name: Audit + +on: + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + schedule: + - cron: '0 0 * * 0' # Once per week + +jobs: + + security_audit: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/bdk-python/bdk-ffi/.github/workflows/cont_integration.yml b/bdk-python/bdk-ffi/.github/workflows/cont_integration.yml new file mode 100644 index 0000000..4422c0d --- /dev/null +++ b/bdk-python/bdk-ffi/.github/workflows/cont_integration.yml @@ -0,0 +1,61 @@ +on: [push, pull_request] + +name: CI + +jobs: + + build-test: + name: Build and test + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - version: 1.63.0 # STABLE + clippy: true + - version: 1.61.0 # MSRV + steps: + - name: checkout + uses: actions/checkout@v2 + - name: Generate cache key + run: echo "${{ matrix.rust.version }} ${{ matrix.features }}" | tee .cache_key + - name: cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - name: Set default toolchain + run: rustup default ${{ matrix.rust.version }} + - name: Set profile + run: rustup set profile minimal + - name: Add clippy + if: ${{ matrix.rust.clippy }} + run: rustup component add clippy + - name: Update toolchain + run: rustup update + - name: Build + run: cargo build + - name: Clippy + if: ${{ matrix.rust.clippy }} + run: cargo clippy --all-targets -- -D warnings + - name: Test + run: CLASSPATH=./tests/jna/jna-5.8.0.jar cargo test + + fmt: + name: Rust fmt + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set default toolchain + run: rustup default nightly + - name: Set profile + run: rustup set profile minimal + - name: Add rustfmt + run: rustup component add rustfmt + - name: Update toolchain + run: rustup update + - name: Check fmt + run: cargo fmt --all -- --config format_code_in_doc_comments=true --check diff --git a/bdk-python/bdk-ffi/.gitignore b/bdk-python/bdk-ffi/.gitignore new file mode 100644 index 0000000..c91c338 --- /dev/null +++ b/bdk-python/bdk-ffi/.gitignore @@ -0,0 +1,17 @@ +target +build +Cargo.lock +/bindings/bdk-kotlin/local.properties +.gradle +wallet_db +bdk_ffi_test +local.properties +*.log +*.dylib +*.so +.DS_Store +testdb +xcuserdata +.lsp +.clj-kondo +.idea/ diff --git a/bdk-python/bdk-ffi/CHANGELOG.md b/bdk-python/bdk-ffi/CHANGELOG.md new file mode 100644 index 0000000..721b0c3 --- /dev/null +++ b/bdk-python/bdk-ffi/CHANGELOG.md @@ -0,0 +1,116 @@ +# Changelog +All notable changes to this project prior to release **0.9.0** are documented in this file. Future +changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`. +Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk-ffi/releases) +page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v0.9.0] +- Breaking Changes + - Rename `get_network()` method on `Wallet` interface to `network()` [#185] + - Rename `get_transactions()` method on `Wallet` interface to `list_transactions()` [#185] + - Remove `generate_extended_key`, returned ExtendedKeyInfo [#154] + - Remove `restore_extended_key`, returned ExtendedKeyInfo [#154] + - Remove dictionary `ExtendedKeyInfo {mnenonic, xprv, fingerprint}` [#154] + - Remove interface `Transaction` [#190] + - Changed `Wallet` interface `list_transaction()` to return array of `TransactionDetails` [#190] + - Update `bdk` dependency version to 0.22 [#193] +- APIs Added [#154] + - `generate_mnemonic()`, returns string mnemonic + - `interface DescriptorSecretKey` + - `new(Network, string_mnenoinc, password)`, contructs DescriptorSecretKey + - `derive(DerivationPath)`, derives and returns child DescriptorSecretKey + - `extend(DerivationPath)`, extends and returns DescriptorSecretKey + - `as_public()`, returns DescriptorSecretKey as DescriptorPublicKey + - `as_string()`, returns DescriptorSecretKey as String + - `interface DescriptorPublicKey` + - `derive(DerivationPath)` derives and returns child DescriptorPublicKey + - `extend(DerivationPath)` extends and returns DescriptorPublicKey + - `as_string()` returns DescriptorPublicKey as String + - Add to `interface Blockchain` the `get_height()` and `get_block_hash()` methods [#184] + - Add to `interface TxBuilder` the `set_recipients(recipient: Vec<AddressAmount>)` method [#186] + - Add to `dictionary TransactionDetails` the `confirmation_time` field [#190] +- Interfaces Added [#154] + - `DescriptorSecretKey` + - `DescriptorPublicKey` + - `DerivationPath` + +[#154]: https://github.com/bitcoindevkit/bdk-ffi/pull/154 +[#184]: https://github.com/bitcoindevkit/bdk-ffi/pull/184 +[#185]: https://github.com/bitcoindevkit/bdk-ffi/pull/185 +[#193]: https://github.com/bitcoindevkit/bdk-ffi/pull/193 + +## [v0.8.0] +- Update BDK to version 0.20.0 [#169] +- APIs Added + - `TxBuilder.add_data(data: Vec<u8>)` [#163] + - `Wallet.list_unspent()` returns `Vec<LocalUtxo>` [#158] + - Add coin control methods on TxBuilder [#164] + +[#163]: https://github.com/bitcoindevkit/bdk-ffi/pull/163 +[#158]: https://github.com/bitcoindevkit/bdk-ffi/pull/158 +[#164]: https://github.com/bitcoindevkit/bdk-ffi/pull/164 +[#169]: https://github.com/bitcoindevkit/bdk-ffi/pull/169 +[#190]: https://github.com/bitcoindevkit/bdk-ffi/pull/190 + +## [v0.7.0] +- Update BDK to version 0.19.0 + - fixes sqlite-db issue causing wrong balance + - adds experimental taproot descriptor and PSBT support +- APIs Removed + - `Wallet.get_new_address()`, returned String, [#137] + - `Wallet.get_last_unused_address()`, returned String [#137] +- APIs Added + - `Wallet.get_address(AddressIndex)`, returns `AddressInfo` [#137] +- APIs Changed + - `Wallet.sign(PartiallySignedBitcoinTransaction)` now returns a bool, true if finalized [#161] + +[#137]: https://github.com/bitcoindevkit/bdk-ffi/pull/137 +[#161]: https://github.com/bitcoindevkit/bdk-ffi/pull/161 + +## [v0.6.0] +- Update BDK to version 0.18.0 +- Add BumpFeeTxBuilder to bump the fee on an unconfirmed tx created by the Wallet +- Change TxBuilder.build() to TxBuilder.finish() to align with bdk function name + +## [v0.5.0] +- Fix Wallet.broadcast function, now returns a tx id as a hex string +- Remove creating a new spending Transaction via the PartiallySignedBitcoinTransaction constructor +- Add TxBuilder for creating new spending PartiallySignedBitcoinTransaction +- Add TxBuilder .add_recipient, .fee_rate, and .build functions +- Add TxBuilder .drain_wallet and .drain_to functions +- Update generate cli tool to generate all binding languages and rename to bdk-ffi-bindgen + +## [v0.4.0] +- Add dual license MIT and Apache 2.0 +- Add sqlite database support +- Fix memory database configuration enum, remove junk field + +## [v0.3.1] +- Remove hard coded sync progress value (was always returning 21.0) + +## [v0.3.0] +- Move bdk-kotlin bindings and ios example to separate repos +- Add bin to generate Python bindings +- Add `PartiallySignedBitcoinTransaction::deserialize` function as named constructor to decode from a string per [BIP 0174] +- Add `PartiallySignedBitcoinTransaction::serialize` function to encode to a string per [BIP 0174] +- Remove `PartiallySignedBitcoinTransaction.details` struct field + +[BIP 0174]:https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#encoding + +## [v0.2.0] + +[unreleased]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.9.0...HEAD +[v0.9.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.8.0...v0.9.0 +[v0.8.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.7.0...v0.8.0 +[v0.7.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.6.0...v0.7.0 +[v0.6.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.5.0...v0.6.0 +[v0.5.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.4.0...v0.5.0 +[v0.4.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.3.1...v0.4.0 +[v0.3.1]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.3.0...v0.3.1 +[v0.3.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.2.0...v0.3.0 +[v0.2.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.0.0...v0.2.0 diff --git a/bdk-python/bdk-ffi/Cargo.toml b/bdk-python/bdk-ffi/Cargo.toml new file mode 100644 index 0000000..63cd96f --- /dev/null +++ b/bdk-python/bdk-ffi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "bdk-ffi" +version = "0.11.0" +authors = ["Steve Myers <steve@notmandatory.org>", "Sudarsan Balaji <sudarsan.balaji@artfuldev.com>"] +edition = "2018" +license = "MIT OR Apache-2.0" + +[workspace] +members = [".","bdk-ffi-bindgen"] +default-members = [".", "bdk-ffi-bindgen"] + +[lib] +crate-type = ["staticlib", "cdylib"] +name = "bdkffi" + +[dependencies] +bdk = { version = "0.24", features = ["all-keys", "use-esplora-ureq", "sqlite-bundled"] } + +uniffi_macros = { version = "0.21.0", features = ["builtin-bindgen"] } +uniffi = { version = "0.21.0", features = ["builtin-bindgen"] } + +[build-dependencies] +uniffi_build = { version = "0.21.0", features = ["builtin-bindgen"] } + +[profile.release-smaller] +inherits = "release" +opt-level = 'z' # Optimize for size. +lto = true # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* diff --git a/bdk-python/bdk-ffi/DEVELOPMENT_CYCLE.md b/bdk-python/bdk-ffi/DEVELOPMENT_CYCLE.md new file mode 100644 index 0000000..00c4094 --- /dev/null +++ b/bdk-python/bdk-ffi/DEVELOPMENT_CYCLE.md @@ -0,0 +1,35 @@ +# Development Cycle + +This project follows a regular releasing schedule similar to the one [used by the Rust language] +except releases always follow the latest [`bdk`] release by one to two weeks. In short, this means +that a new release is made at a regular cadence, with all the feature/bugfixes that made it to +`master` in time. This ensures that we don't keep delaying releases waiting for +"just one more little thing". + +After making a new `bdk-ffi` release tag all downstream language bindings should also be updated. + +This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it +is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be +considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API +changes in the changelog info attached to each release tag. + +We decided to maintain a faster release cycle while the library is still in "beta", i.e. before +release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing +issues, we want developers to have access to those updates as fast as possible. For this reason we +will make a release **every 4 weeks**. + +Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer +release cycles of **6 weeks**. + +The "feature freeze" will happen when [`bdk`] releases a release candidate. This project will then +be updated and tested with [`bdk`] release candidates until a final release is published. This +means a new branch will be created originating from the `master` tip at that time, and in that +branch we will stop adding new features and only focus on ensuring the ones we've added are working +properly. + +To create a new release a release manager will create a new issue using a `Release` template and +follow the template instructions. + +[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html +[Semantic Versioning]: https://semver.org/ +[`bdk`]: https://github.com/bitcoindevkit/bdk diff --git a/bdk-python/bdk-ffi/LICENSE b/bdk-python/bdk-ffi/LICENSE new file mode 100644 index 0000000..9c61848 --- /dev/null +++ b/bdk-python/bdk-ffi/LICENSE @@ -0,0 +1,14 @@ +This software is licensed under [Apache 2.0](LICENSE-APACHE) or +[MIT](LICENSE-MIT), at your option. + +Some files retain their own copyright notice, however, for full authorship +information, see version control history. + +Except as otherwise noted in individual files, all files in this repository are +licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or +http://opensource.org/licenses/MIT>, at your option. + +You may not use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of this software or any files in this repository except in +accordance with one or both of these licenses. \ No newline at end of file diff --git a/bdk-python/bdk-ffi/LICENSE-APACHE b/bdk-python/bdk-ffi/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/bdk-python/bdk-ffi/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bdk-python/bdk-ffi/LICENSE-MIT b/bdk-python/bdk-ffi/LICENSE-MIT new file mode 100644 index 0000000..9d982a4 --- /dev/null +++ b/bdk-python/bdk-ffi/LICENSE-MIT @@ -0,0 +1,16 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bdk-python/bdk-ffi/README.md b/bdk-python/bdk-ffi/README.md new file mode 100644 index 0000000..c47fd5c --- /dev/null +++ b/bdk-python/bdk-ffi/README.md @@ -0,0 +1,75 @@ +# Native language bindings for BDK + +<p> + <a href="https://github.com/bitcoindevkit/bdk-ffi/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a> + <a href="https://github.com/bitcoindevkit/bdk-ffi/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk-ffi/workflows/CI/badge.svg"></a> + <a href="https://blog.rust-lang.org/2022/05/19/Rust-1.61.0.html"><img alt="Rustc Version 1.61.0+" src="https://img.shields.io/badge/rustc-1.61.0%2B-lightgrey.svg"/></a> + <a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a> + </p> + +The workspace in this repository creates the `libbdkffi` multi-language library for the rust based +[bdk] library from the [Bitcoin Dev Kit] project. The `bdk-ffi-bindgen` package builds a tool for +generating the actual language binding code used to access the `libbdkffi` library. + +Each supported language has its own repository that includes this project as a [git submodule]. +The rust code in this project is a wrapper around the [bdk] library to expose it's APIs in a +uniform way using the [mozilla/uniffi-rs] bindings generator for each supported target language. + +## Supported target languages and platforms + +The below repositories include instructions for using, building, and publishing the native +language binding for [bdk] supported by this project. + +| Language | Platform | Repository | +| -------- | ------------ | ------------ | +| Kotlin | jvm | [bdk-kotlin] | +| Kotlin | android | [bdk-kotlin] | +| Swift | iOS, macOS | [bdk-swift] | +| Python | linux, macOS | [bdk-python] | + +## Language bindings generator tool + +Use the `bdk-ffi-bindgen` tool to generate language binding code for the above supported languages. +To run `bdk-ffi-bindgen` and see the available options use the command: +```shell +cargo run -p bdk-ffi-bindgen -- --help +``` + +[bdk]: https://github.com/bitcoindevkit/bdk +[Bitcoin Dev Kit]: https://github.com/bitcoindevkit +[git submodule]: https://git-scm.com/book/en/v2/Git-Tools-Submodules +[uniffi-rs]: https://github.com/mozilla/uniffi-rs + +[bdk-kotlin]: https://github.com/bitcoindevkit/bdk-kotlin +[bdk-swift]: https://github.com/bitcoindevkit/bdk-swift +[bdk-python]: https://github.com/bitcoindevkit/bdk-python + +## Contributing + +### Adding new structs and functions + +See the [UniFFI User Guide](https://mozilla.github.io/uniffi-rs/) + +#### For pass by value objects + +1. create new rust struct with only fields that are supported UniFFI types +1. update mapping `bdk.udl` file with new `dictionary` + +#### For pass by reference values + +1. create wrapper rust struct/impl with only fields that are `Sync + Send` +1. update mapping `bdk.udl` file with new `interface` + +## Goals + +1. Language bindings should feel idiomatic in target languages/platforms +1. Adding new targets should be easy +1. Getting up and running should be easy +1. Contributing should be easy +1. Get it right, then automate + +## Thanks + +This project is made possible thanks to the wonderful work by the [mozilla/uniffi-rs] team. + +[mozilla/uniffi-rs]: https://github.com/mozilla/uniffi-rs diff --git a/bdk-python/bdk-ffi/bdk-ffi-bindgen/Cargo.toml b/bdk-python/bdk-ffi/bdk-ffi-bindgen/Cargo.toml new file mode 100644 index 0000000..87ee0bc --- /dev/null +++ b/bdk-python/bdk-ffi/bdk-ffi-bindgen/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bdk-ffi-bindgen" +version = "0.2.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.45" # remove after upgrading to next version of uniffi +structopt = "0.3" +uniffi_bindgen = "0.21.0" +camino = "1.0.9" diff --git a/bdk-python/bdk-ffi/bdk-ffi-bindgen/src/main.rs b/bdk-python/bdk-ffi/bdk-ffi-bindgen/src/main.rs new file mode 100644 index 0000000..8a87fc5 --- /dev/null +++ b/bdk-python/bdk-ffi/bdk-ffi-bindgen/src/main.rs @@ -0,0 +1,138 @@ +use camino::Utf8Path; +use std::fmt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use structopt::StructOpt; + +#[derive(Debug, Eq, PartialEq)] +pub enum Language { + Kotlin, + Python, + Swift, +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Language::Kotlin => write!(f, "kotlin"), + Language::Swift => write!(f, "swift"), + Language::Python => write!(f, "python"), + } + } +} + +#[derive(Debug)] +pub enum Error { + UnsupportedLanguage, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl FromStr for Language { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "kotlin" => Ok(Language::Kotlin), + "python" => Ok(Language::Python), + "swift" => Ok(Language::Swift), + _ => Err(Error::UnsupportedLanguage), + } + } +} + +fn generate_bindings(opt: &Opt) -> anyhow::Result<(), anyhow::Error> { + let path: &Utf8Path = Utf8Path::from_path(&opt.udl_file).unwrap(); + let out_dir: &Utf8Path = Utf8Path::from_path(&opt.out_dir).unwrap(); + uniffi_bindgen::generate_bindings( + path, + None, + vec![opt.language.to_string().as_str()], + Some(out_dir), + None, + false, + )?; + + Ok(()) +} + +fn fixup_python_lib_path( + out_dir: &Path, + lib_name: &Path, +) -> Result<(), Box<dyn std::error::Error>> { + use std::fs; + use std::io::Write; + + const LOAD_INDIRECT_DEF: &str = "def loadIndirect():"; + + let bindings_file = out_dir.join("bdk.py"); + let mut data = fs::read_to_string(&bindings_file)?; + + let pos = data + .find(LOAD_INDIRECT_DEF) + .unwrap_or_else(|| panic!("loadIndirect not found in `{}`", bindings_file.display())); + let range = pos..pos + LOAD_INDIRECT_DEF.len(); + + let replacement = format!( + r#" +def loadIndirect(): + import glob + return getattr(ctypes.cdll, glob.glob(os.path.join(os.path.dirname(os.path.abspath(__file__)), '{}.*'))[0]) + +def _loadIndirectOld():"#, + &lib_name.to_str().expect("lib name") + ); + data.replace_range(range, &replacement); + + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(&bindings_file)?; + file.write_all(data.as_bytes())?; + + Ok(()) +} + +#[derive(Debug, StructOpt)] +#[structopt( + name = "bdk-ffi-bindgen", + about = "A tool to generate bdk-ffi language bindings" +)] +struct Opt { + /// UDL file + #[structopt(env = "BDKFFI_BINDGEN_UDL", short, long, default_value("src/bdk.udl"), parse(try_from_str = PathBuf::from_str))] + udl_file: PathBuf, + + /// Language to generate bindings for + #[structopt(env = "BDKFFI_BINDGEN_LANGUAGE", short, long, possible_values(&["kotlin","swift","python"]), parse(try_from_str = Language::from_str))] + language: Language, + + /// Output directory to put generated language bindings + #[structopt(env = "BDKFFI_BINDGEN_OUTPUT_DIR", short, long, parse(try_from_str = PathBuf::from_str))] + out_dir: PathBuf, + + /// Python fix up lib path + #[structopt(env = "BDKFFI_BINDGEN_PYTHON_FIXUP_PATH", short, long, parse(try_from_str = PathBuf::from_str))] + python_fixup_path: Option<PathBuf>, +} + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let opt = Opt::from_args(); + + println!("Input UDL file is {:?}", opt.udl_file); + println!("Chosen language is {}", opt.language); + println!("Output directory is {:?}", opt.out_dir); + + generate_bindings(&opt)?; + + if opt.language == Language::Python { + if let Some(path) = opt.python_fixup_path { + println!("Fixing up python lib path, {:?}", &path); + fixup_python_lib_path(&opt.out_dir, &path)?; + } + } + Ok(()) +} diff --git a/bdk-python/bdk-ffi/build.rs b/bdk-python/bdk-ffi/build.rs new file mode 100644 index 0000000..153077f --- /dev/null +++ b/bdk-python/bdk-ffi/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi_build::generate_scaffolding("src/bdk.udl").unwrap(); +} diff --git a/bdk-python/bdk-ffi/src/bdk.udl b/bdk-python/bdk-ffi/src/bdk.udl new file mode 100644 index 0000000..e45fe87 --- /dev/null +++ b/bdk-python/bdk-ffi/src/bdk.udl @@ -0,0 +1,337 @@ +namespace bdk { + +}; + +[Error] +enum BdkError { + "InvalidU32Bytes", + "Generic", + "MissingCachedScripts", + "ScriptDoesntHaveAddressForm", + "NoRecipients", + "NoUtxosSelected", + "OutputBelowDustLimit", + "InsufficientFunds", + "BnBTotalTriesExceeded", + "BnBNoExactMatch", + "UnknownUtxo", + "TransactionNotFound", + "TransactionConfirmed", + "IrreplaceableTransaction", + "FeeRateTooLow", + "FeeTooLow", + "FeeRateUnavailable", + "MissingKeyOrigin", + "Key", + "ChecksumMismatch", + "SpendingPolicyRequired", + "InvalidPolicyPathError", + "Signer", + "InvalidNetwork", + "InvalidProgressValue", + "ProgressUpdateError", + "InvalidOutpoint", + "Descriptor", + "Encode", + "Miniscript", + "MiniscriptPsbt", + "Bip32", + "Secp256k1", + "Json", + "Hex", + "Psbt", + "PsbtParse", + "Electrum", + "Esplora", + "Sled", + "Rusqlite", +}; + +dictionary AddressInfo { + u32 index; + string address; +}; + +enum AddressIndex { + "New", + "LastUnused", +}; + +enum Network { + "Bitcoin", + "Testnet", + "Signet", + "Regtest", +}; + +dictionary SledDbConfiguration { + string path; + string tree_name; +}; + +dictionary SqliteDbConfiguration { + string path; +}; + +dictionary Balance { + u64 immature; + u64 trusted_pending; + u64 untrusted_pending; + u64 confirmed; + u64 spendable; + u64 total; +}; + +[Enum] +interface DatabaseConfig { + Memory(); + Sled(SledDbConfiguration config); + Sqlite(SqliteDbConfiguration config); +}; + +dictionary TransactionDetails { + u64? fee; + u64 received; + u64 sent; + string txid; + BlockTime? confirmation_time; +}; + +dictionary BlockTime { + u32 height; + u64 timestamp; +}; + +enum WordCount { + "Words12", + "Words15", + "Words18", + "Words21", + "Words24", +}; + +dictionary ElectrumConfig { + string url; + string? socks5; + u8 retry; + u8? timeout; + u64 stop_gap; +}; + +dictionary EsploraConfig { + string base_url; + string? proxy; + u8? concurrency; + u64 stop_gap; + u64? timeout; +}; + +[Enum] +interface BlockchainConfig { + Electrum(ElectrumConfig config); + Esplora(EsploraConfig config); +}; + +interface Blockchain { + [Throws=BdkError] + constructor(BlockchainConfig config); + + [Throws=BdkError] + void broadcast([ByRef] PartiallySignedTransaction psbt); + + [Throws=BdkError] + u32 get_height(); + + [Throws=BdkError] + string get_block_hash(u32 height); +}; + +callback interface Progress { + void update(f32 progress, string? message); +}; + +dictionary OutPoint { + string txid; + u32 vout; +}; + +dictionary TxOut { + u64 value; + string address; +}; + +enum KeychainKind { + "External", + "Internal", +}; + +dictionary LocalUtxo { + OutPoint outpoint; + TxOut txout; + KeychainKind keychain; + boolean is_spent; +}; + +dictionary ScriptAmount { + Script script; + u64 amount; +}; + +interface Wallet { + [Throws=BdkError] + constructor(string descriptor, string? change_descriptor, Network network, DatabaseConfig database_config); + + [Throws=BdkError] + AddressInfo get_address(AddressIndex address_index); + + [Throws=BdkError] + Balance get_balance(); + + [Throws=BdkError] + boolean sign([ByRef] PartiallySignedTransaction psbt); + + [Throws=BdkError] + sequence<TransactionDetails> list_transactions(); + + Network network(); + + [Throws=BdkError] + void sync([ByRef] Blockchain blockchain, Progress? progress); + + [Throws=BdkError] + sequence<LocalUtxo> list_unspent(); +}; + +interface FeeRate { + [Name=from_sat_per_vb] + constructor(float sat_per_vb); + + float as_sat_per_vb(); +}; + +interface PartiallySignedTransaction { + [Throws=BdkError] + constructor(string psbt_base64); + + string serialize(); + + string txid(); + + sequence<u8> extract_tx(); + + [Throws=BdkError] + PartiallySignedTransaction combine(PartiallySignedTransaction other); + + u64? fee_amount(); + + FeeRate? fee_rate(); +}; + +dictionary TxBuilderResult { + PartiallySignedTransaction psbt; + TransactionDetails transaction_details; +}; + +interface TxBuilder { + constructor(); + + TxBuilder add_recipient(Script script, u64 amount); + + TxBuilder add_unspendable(OutPoint unspendable); + + TxBuilder add_utxo(OutPoint outpoint); + + TxBuilder add_utxos(sequence<OutPoint> outpoints); + + TxBuilder do_not_spend_change(); + + TxBuilder manually_selected_only(); + + TxBuilder only_spend_change(); + + TxBuilder unspendable(sequence<OutPoint> unspendable); + + TxBuilder fee_rate(float sat_per_vbyte); + + TxBuilder fee_absolute(u64 fee_amount); + + TxBuilder drain_wallet(); + + TxBuilder drain_to(string address); + + TxBuilder enable_rbf(); + + TxBuilder enable_rbf_with_sequence(u32 nsequence); + + TxBuilder add_data(sequence<u8> data); + + TxBuilder set_recipients(sequence<ScriptAmount> recipients); + + [Throws=BdkError] + TxBuilderResult finish([ByRef] Wallet wallet); +}; + +interface BumpFeeTxBuilder { + constructor(string txid, float new_fee_rate); + + BumpFeeTxBuilder allow_shrinking(string address); + + BumpFeeTxBuilder enable_rbf(); + + BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence); + + [Throws=BdkError] + PartiallySignedTransaction finish([ByRef] Wallet wallet); +}; + +interface Mnemonic { + constructor(WordCount word_count); + + [Name=from_string, Throws=BdkError] + constructor(string mnemonic); + + [Name=from_entropy, Throws=BdkError] + constructor(sequence<u8> entropy); + + string as_string(); +}; + +interface DerivationPath { + [Throws=BdkError] + constructor(string path); +}; + +interface DescriptorSecretKey { + constructor(Network network, Mnemonic mnemonic, string? password); + + [Throws=BdkError] + DescriptorSecretKey derive(DerivationPath path); + + DescriptorSecretKey extend(DerivationPath path); + + DescriptorPublicKey as_public(); + + sequence<u8> secret_bytes(); + + string as_string(); +}; + +interface DescriptorPublicKey { + [Throws=BdkError] + DescriptorPublicKey derive(DerivationPath path); + + DescriptorPublicKey extend(DerivationPath path); + + string as_string(); +}; + +interface Address { + [Throws=BdkError] + constructor(string address); + + Script script_pubkey(); +}; + +interface Script { + constructor(sequence<u8> raw_output_script); +}; diff --git a/bdk-python/bdk-ffi/src/lib.rs b/bdk-python/bdk-ffi/src/lib.rs new file mode 100644 index 0000000..e062281 --- /dev/null +++ b/bdk-python/bdk-ffi/src/lib.rs @@ -0,0 +1,1293 @@ +use bdk::bitcoin::blockdata::script::Script as BdkScript; +use bdk::bitcoin::hashes::hex::ToHex; +use bdk::bitcoin::secp256k1::Secp256k1; +use bdk::bitcoin::util::bip32::DerivationPath as BdkDerivationPath; +use bdk::bitcoin::util::psbt::serialize::Serialize; +use bdk::bitcoin::util::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction; +use bdk::bitcoin::Sequence; +use bdk::bitcoin::{Address as BdkAddress, Network, OutPoint as BdkOutPoint, Txid}; +use bdk::blockchain::any::{AnyBlockchain, AnyBlockchainConfig}; +use bdk::blockchain::GetBlockHash; +use bdk::blockchain::GetHeight; +use bdk::blockchain::{ + electrum::ElectrumBlockchainConfig, esplora::EsploraBlockchainConfig, ConfigurableBlockchain, +}; +use bdk::blockchain::{Blockchain as BdkBlockchain, Progress as BdkProgress}; +use bdk::database::any::{AnyDatabase, SledDbConfiguration, SqliteDbConfiguration}; +use bdk::database::{AnyDatabaseConfig, ConfigurableDatabase}; +use bdk::descriptor::DescriptorXKey; +use bdk::keys::bip39::{Language, Mnemonic as BdkMnemonic, WordCount}; +use bdk::keys::{ + DerivableKey, DescriptorPublicKey as BdkDescriptorPublicKey, + DescriptorSecretKey as BdkDescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey, +}; +use bdk::miniscript::BareCtx; +use bdk::psbt::PsbtUtils; +use bdk::wallet::tx_builder::ChangeSpendPolicy; +use bdk::wallet::AddressIndex as BdkAddressIndex; +use bdk::wallet::AddressInfo as BdkAddressInfo; +use bdk::{ + Balance as BdkBalance, BlockTime, Error as BdkError, FeeRate, KeychainKind, SignOptions, + SyncOptions as BdkSyncOptions, Wallet as BdkWallet, +}; +use std::collections::HashSet; +use std::convert::{From, TryFrom}; +use std::fmt; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::{Arc, Mutex, MutexGuard}; + +uniffi_macros::include_scaffolding!("bdk"); + +/// A output script and an amount of satoshis. +pub struct ScriptAmount { + pub script: Arc<Script>, + pub amount: u64, +} + +/// A derived address and the index it was found at. +pub struct AddressInfo { + /// Child index of this address + pub index: u32, + /// Address + pub address: String, +} + +impl From<BdkAddressInfo> for AddressInfo { + fn from(x: bdk::wallet::AddressInfo) -> AddressInfo { + AddressInfo { + index: x.index, + address: x.address.to_string(), + } + } +} + +/// The address index selection strategy to use to derived an address from the wallet's external +/// descriptor. +pub enum AddressIndex { + /// Return a new address after incrementing the current descriptor index. + New, + /// Return the address for the current descriptor index if it has not been used in a received + /// transaction. Otherwise return a new address as with AddressIndex::New. + /// Use with caution, if the wallet has not yet detected an address has been used it could + /// return an already used address. This function is primarily meant for situations where the + /// caller is untrusted; for example when deriving donation addresses on-demand for a public + /// web page. + LastUnused, +} + +impl From<AddressIndex> for BdkAddressIndex { + fn from(x: AddressIndex) -> BdkAddressIndex { + match x { + AddressIndex::New => BdkAddressIndex::New, + AddressIndex::LastUnused => BdkAddressIndex::LastUnused, + } + } +} + +/// Type that can contain any of the database configurations defined by the library +/// This allows storing a single configuration that can be loaded into an AnyDatabaseConfig +/// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime +/// will find this particularly useful. +pub enum DatabaseConfig { + /// Memory database has no config + Memory, + /// Simple key-value embedded database based on sled + Sled { config: SledDbConfiguration }, + /// Sqlite embedded database using rusqlite + Sqlite { config: SqliteDbConfiguration }, +} + +/// Configuration for an ElectrumBlockchain +pub struct ElectrumConfig { + /// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with ssl:// or tcp:// and include a port + /// e.g. ssl://electrum.blockstream.info:60002 + pub url: String, + /// URL of the socks5 proxy server or a Tor service + pub socks5: Option<String>, + /// Request retry count + pub retry: u8, + /// Request timeout (seconds) + pub timeout: Option<u8>, + /// Stop searching addresses for transactions after finding an unused gap of this length + pub stop_gap: u64, +} + +/// Configuration for an EsploraBlockchain +pub struct EsploraConfig { + /// Base URL of the esplora service + /// e.g. https://blockstream.info/api/ + pub base_url: String, + /// Optional URL of the proxy to use to make requests to the Esplora server + /// The string should be formatted as: <protocol>://<user>:<password>@host:<port>. + /// Note that the format of this value and the supported protocols change slightly between the + /// sync version of esplora (using ureq) and the async version (using reqwest). For more + /// details check with the documentation of the two crates. Both of them are compiled with + /// the socks feature enabled. + /// The proxy is ignored when targeting wasm32. + pub proxy: Option<String>, + /// Number of parallel requests sent to the esplora service (default: 4) + pub concurrency: Option<u8>, + /// Stop searching addresses for transactions after finding an unused gap of this length. + pub stop_gap: u64, + /// Socket timeout. + pub timeout: Option<u64>, +} + +/// Type that can contain any of the blockchain configurations defined by the library. +pub enum BlockchainConfig { + /// Electrum client + Electrum { config: ElectrumConfig }, + /// Esplora client + Esplora { config: EsploraConfig }, +} + +/// A wallet transaction +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TransactionDetails { + /// Transaction id. + pub txid: String, + /// Received value (sats) + /// Sum of owned outputs of this transaction. + pub received: u64, + /// Sent value (sats) + /// Sum of owned inputs of this transaction. + pub sent: u64, + /// Fee value (sats) if confirmed. + /// The availability of the fee depends on the backend. It's never None with an Electrum + /// Server backend, but it could be None with a Bitcoin RPC node without txindex that receive + /// funds while offline. + pub fee: Option<u64>, + /// If the transaction is confirmed, contains height and timestamp of the block containing the + /// transaction, unconfirmed transaction contains `None`. + pub confirmation_time: Option<BlockTime>, +} + +impl From<&bdk::TransactionDetails> for TransactionDetails { + fn from(x: &bdk::TransactionDetails) -> TransactionDetails { + TransactionDetails { + fee: x.fee, + txid: x.txid.to_string(), + received: x.received, + sent: x.sent, + confirmation_time: x.confirmation_time.clone(), + } + } +} + +struct Blockchain { + blockchain_mutex: Mutex<AnyBlockchain>, +} + +impl Blockchain { + fn new(blockchain_config: BlockchainConfig) -> Result<Self, BdkError> { + let any_blockchain_config = match blockchain_config { + BlockchainConfig::Electrum { config } => { + AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { + retry: config.retry, + socks5: config.socks5, + timeout: config.timeout, + url: config.url, + stop_gap: usize::try_from(config.stop_gap).unwrap(), + }) + } + BlockchainConfig::Esplora { config } => { + AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { + base_url: config.base_url, + proxy: config.proxy, + concurrency: config.concurrency, + stop_gap: usize::try_from(config.stop_gap).unwrap(), + timeout: config.timeout, + }) + } + }; + let blockchain = AnyBlockchain::from_config(&any_blockchain_config)?; + Ok(Self { + blockchain_mutex: Mutex::new(blockchain), + }) + } + + fn get_blockchain(&self) -> MutexGuard<AnyBlockchain> { + self.blockchain_mutex.lock().expect("blockchain") + } + + fn broadcast(&self, psbt: &PartiallySignedTransaction) -> Result<(), BdkError> { + let tx = psbt.internal.lock().unwrap().clone().extract_tx(); + self.get_blockchain().broadcast(&tx) + } + + fn get_height(&self) -> Result<u32, BdkError> { + self.get_blockchain().get_height() + } + + fn get_block_hash(&self, height: u32) -> Result<String, BdkError> { + self.get_blockchain() + .get_block_hash(u64::from(height)) + .map(|hash| hash.to_string()) + } +} + +struct Wallet { + wallet_mutex: Mutex<BdkWallet<AnyDatabase>>, +} + +/// A reference to a transaction output. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct OutPoint { + /// The referenced transaction's txid. + txid: String, + /// The index of the referenced output in its transaction's vout. + vout: u32, +} + +impl From<&OutPoint> for BdkOutPoint { + fn from(x: &OutPoint) -> BdkOutPoint { + BdkOutPoint { + txid: Txid::from_str(&x.txid).unwrap(), + vout: x.vout, + } + } +} + +pub struct Balance { + // All coinbase outputs not yet matured + pub immature: u64, + /// Unconfirmed UTXOs generated by a wallet tx + pub trusted_pending: u64, + /// Unconfirmed UTXOs received from an external wallet + pub untrusted_pending: u64, + /// Confirmed and immediately spendable balance + pub confirmed: u64, + /// Get sum of trusted_pending and confirmed coins + pub spendable: u64, + /// Get the whole balance visible to the wallet + pub total: u64, +} + +impl From<BdkBalance> for Balance { + fn from(bdk_balance: BdkBalance) -> Self { + Balance { + immature: bdk_balance.immature, + trusted_pending: bdk_balance.trusted_pending, + untrusted_pending: bdk_balance.untrusted_pending, + confirmed: bdk_balance.confirmed, + spendable: bdk_balance.get_spendable(), + total: bdk_balance.get_total(), + } + } +} + +/// A transaction output, which defines new coins to be created from old ones. +pub struct TxOut { + /// The value of the output, in satoshis. + value: u64, + /// The address of the output. + address: String, +} + +pub struct LocalUtxo { + outpoint: OutPoint, + txout: TxOut, + keychain: KeychainKind, + is_spent: bool, +} + +// This trait is used to convert the bdk TxOut type with field `script_pubkey: Script` +// into the bdk-ffi TxOut type which has a field `address: String` instead +trait NetworkLocalUtxo { + fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo; +} + +impl NetworkLocalUtxo for LocalUtxo { + fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo { + LocalUtxo { + outpoint: OutPoint { + txid: x.outpoint.txid.to_string(), + vout: x.outpoint.vout, + }, + txout: TxOut { + value: x.txout.value, + address: BdkAddress::from_script(&x.txout.script_pubkey, network) + .unwrap() + .to_string(), + }, + keychain: x.keychain, + is_spent: x.is_spent, + } + } +} + +/// Trait that logs at level INFO every update received (if any). +pub trait Progress: Send + Sync + 'static { + /// Send a new progress update. The progress value should be in the range 0.0 - 100.0, and the message value is an + /// optional text message that can be displayed to the user. + fn update(&self, progress: f32, message: Option<String>); +} + +struct ProgressHolder { + progress: Box<dyn Progress>, +} + +impl BdkProgress for ProgressHolder { + fn update(&self, progress: f32, message: Option<String>) -> Result<(), BdkError> { + self.progress.update(progress, message); + Ok(()) + } +} + +impl fmt::Debug for ProgressHolder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProgressHolder").finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub struct PartiallySignedTransaction { + internal: Mutex<BdkPartiallySignedTransaction>, +} + +impl PartiallySignedTransaction { + fn new(psbt_base64: String) -> Result<Self, BdkError> { + let psbt: BdkPartiallySignedTransaction = + BdkPartiallySignedTransaction::from_str(&psbt_base64)?; + Ok(PartiallySignedTransaction { + internal: Mutex::new(psbt), + }) + } + + fn serialize(&self) -> String { + let psbt = self.internal.lock().unwrap().clone(); + psbt.to_string() + } + + fn txid(&self) -> String { + let tx = self.internal.lock().unwrap().clone().extract_tx(); + let txid = tx.txid(); + txid.to_hex() + } + + /// Return the transaction as bytes. + fn extract_tx(&self) -> Vec<u8> { + self.internal + .lock() + .unwrap() + .clone() + .extract_tx() + .serialize() + } + + /// Combines this PartiallySignedTransaction with other PSBT as described by BIP 174. + /// + /// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)` + fn combine( + &self, + other: Arc<PartiallySignedTransaction>, + ) -> Result<Arc<PartiallySignedTransaction>, BdkError> { + let other_psbt = other.internal.lock().unwrap().clone(); + let mut original_psbt = self.internal.lock().unwrap().clone(); + + original_psbt.combine(other_psbt)?; + Ok(Arc::new(PartiallySignedTransaction { + internal: Mutex::new(original_psbt), + })) + } + + /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_amount(&self) -> Option<u64> { + self.internal.lock().unwrap().fee_amount() + } + + /// The transaction's fee rate. This value will only be accurate if calculated AFTER the + /// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the + /// transaction. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_rate(&self) -> Option<Arc<FeeRate>> { + self.internal.lock().unwrap().fee_rate().map(Arc::new) + } +} + +/// A Bitcoin wallet. +/// The Wallet acts as a way of coherently interfacing with output descriptors and related transactions. Its main components are: +/// 1. Output descriptors from which it can derive addresses. +/// 2. A Database where it tracks transactions and utxos related to the descriptors. +/// 3. Signers that can contribute signatures to addresses instantiated from the descriptors. +impl Wallet { + fn new( + descriptor: String, + change_descriptor: Option<String>, + network: Network, + database_config: DatabaseConfig, + ) -> Result<Self, BdkError> { + let any_database_config = match database_config { + DatabaseConfig::Memory => AnyDatabaseConfig::Memory(()), + DatabaseConfig::Sled { config } => AnyDatabaseConfig::Sled(config), + DatabaseConfig::Sqlite { config } => AnyDatabaseConfig::Sqlite(config), + }; + let database = AnyDatabase::from_config(&any_database_config)?; + let wallet_mutex = Mutex::new(BdkWallet::new( + &descriptor, + change_descriptor.as_ref(), + network, + database, + )?); + Ok(Wallet { wallet_mutex }) + } + + fn get_wallet(&self) -> MutexGuard<BdkWallet<AnyDatabase>> { + self.wallet_mutex.lock().expect("wallet") + } + + /// Get the Bitcoin network the wallet is using. + fn network(&self) -> Network { + self.get_wallet().network() + } + + /// Sync the internal database with the blockchain. + fn sync( + &self, + blockchain: &Blockchain, + progress: Option<Box<dyn Progress>>, + ) -> Result<(), BdkError> { + let bdk_sync_opts = BdkSyncOptions { + progress: progress.map(|p| { + Box::new(ProgressHolder { progress: p }) + as Box<(dyn bdk::blockchain::Progress + 'static)> + }), + }; + + let blockchain = blockchain.get_blockchain(); + self.get_wallet().sync(blockchain.deref(), bdk_sync_opts) + } + + /// Return a derived address using the external descriptor, see AddressIndex for available address index selection + /// strategies. If none of the keys in the descriptor are derivable (i.e. the descriptor does not end with a * character) + /// then the same address will always be returned for any AddressIndex. + fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, BdkError> { + self.get_wallet() + .get_address(address_index.into()) + .map(AddressInfo::from) + } + + /// Return the balance, meaning the sum of this wallet’s unspent outputs’ values. Note that this method only operates + /// on the internal database, which first needs to be Wallet.sync manually. + fn get_balance(&self) -> Result<Balance, BdkError> { + self.get_wallet().get_balance().map(|b| b.into()) + } + + /// Sign a transaction with all the wallet’s signers. + fn sign(&self, psbt: &PartiallySignedTransaction) -> Result<bool, BdkError> { + let mut psbt = psbt.internal.lock().unwrap(); + self.get_wallet().sign(&mut psbt, SignOptions::default()) + } + + /// Return the list of transactions made and received by the wallet. Note that this method only operate on the internal database, which first needs to be [Wallet.sync] manually. + fn list_transactions(&self) -> Result<Vec<TransactionDetails>, BdkError> { + let transaction_details = self.get_wallet().list_transactions(true)?; + Ok(transaction_details + .iter() + .map(TransactionDetails::from) + .collect()) + } + + /// Return the list of unspent outputs of this wallet. Note that this method only operates on the internal database, + /// which first needs to be Wallet.sync manually. + fn list_unspent(&self) -> Result<Vec<LocalUtxo>, BdkError> { + let unspents = self.get_wallet().list_unspent()?; + Ok(unspents + .iter() + .map(|u| LocalUtxo::from_utxo(u, self.network())) + .collect()) + } +} + +fn to_script_pubkey(address: &str) -> Result<BdkScript, BdkError> { + BdkAddress::from_str(address) + .map(|x| x.script_pubkey()) + .map_err(|e| BdkError::Generic(e.to_string())) +} + +/// A Bitcoin address. +struct Address { + address: BdkAddress, +} + +impl Address { + fn new(address: String) -> Result<Self, BdkError> { + BdkAddress::from_str(address.as_str()) + .map(|a| Address { address: a }) + .map_err(|e| BdkError::Generic(e.to_string())) + } + + fn script_pubkey(&self) -> Arc<Script> { + Arc::new(Script { + script: self.address.script_pubkey(), + }) + } +} + +/// A Bitcoin script. +#[derive(Clone)] +pub struct Script { + script: BdkScript, +} + +impl Script { + fn new(raw_output_script: Vec<u8>) -> Self { + let script: BdkScript = BdkScript::from(raw_output_script); + Script { script } + } +} + +#[derive(Clone, Debug)] +enum RbfValue { + Default, + Value(u32), +} + +/// The result after calling the TxBuilder finish() function. Contains unsigned PSBT and +/// transaction details. +pub struct TxBuilderResult { + pub psbt: Arc<PartiallySignedTransaction>, + pub transaction_details: TransactionDetails, +} + +/// A transaction builder. +/// After creating the TxBuilder, you set options on it until finally calling finish to consume the builder and generate the transaction. +/// Each method on the TxBuilder returns an instance of a new TxBuilder with the option set/added. +#[derive(Clone, Debug)] +struct TxBuilder { + recipients: Vec<(BdkScript, u64)>, + utxos: Vec<OutPoint>, + unspendable: HashSet<OutPoint>, + change_policy: ChangeSpendPolicy, + manually_selected_only: bool, + fee_rate: Option<f32>, + fee_absolute: Option<u64>, + drain_wallet: bool, + drain_to: Option<String>, + rbf: Option<RbfValue>, + data: Vec<u8>, +} + +impl TxBuilder { + fn new() -> Self { + TxBuilder { + recipients: Vec::new(), + utxos: Vec::new(), + unspendable: HashSet::new(), + change_policy: ChangeSpendPolicy::ChangeAllowed, + manually_selected_only: false, + fee_rate: None, + fee_absolute: None, + drain_wallet: false, + drain_to: None, + rbf: None, + data: Vec::new(), + } + } + + /// Add a recipient to the internal list. + fn add_recipient(&self, script: Arc<Script>, amount: u64) -> Arc<Self> { + let mut recipients: Vec<(BdkScript, u64)> = self.recipients.clone(); + recipients.append(&mut vec![(script.script.clone(), amount)]); + Arc::new(TxBuilder { + recipients, + ..self.clone() + }) + } + + fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> { + let recipients = recipients + .iter() + .map(|script_amount| (script_amount.script.script.clone(), script_amount.amount)) + .collect(); + Arc::new(TxBuilder { + recipients, + ..self.clone() + }) + } + + /// Add a utxo to the internal list of unspendable utxos. It’s important to note that the "must-be-spent" + /// utxos added with [TxBuilder.addUtxo] have priority over this. See the Rust docs of the two linked methods for more details. + fn add_unspendable(&self, unspendable: OutPoint) -> Arc<Self> { + let mut unspendable_hash_set = self.unspendable.clone(); + unspendable_hash_set.insert(unspendable); + Arc::new(TxBuilder { + unspendable: unspendable_hash_set, + ..self.clone() + }) + } + + /// Add an outpoint to the internal list of UTXOs that must be spent. These have priority over the "unspendable" + /// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent. + fn add_utxo(&self, outpoint: OutPoint) -> Arc<Self> { + self.add_utxos(vec![outpoint]) + } + + /// Add the list of outpoints to the internal list of UTXOs that must be spent. If an error occurs while adding + /// any of the UTXOs then none of them are added and the error is returned. These have priority over the "unspendable" + /// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent. + fn add_utxos(&self, mut outpoints: Vec<OutPoint>) -> Arc<Self> { + let mut utxos = self.utxos.to_vec(); + utxos.append(&mut outpoints); + Arc::new(TxBuilder { + utxos, + ..self.clone() + }) + } + + /// Do not spend change outputs. This effectively adds all the change outputs to the "unspendable" list. See TxBuilder.unspendable. + fn do_not_spend_change(&self) -> Arc<Self> { + Arc::new(TxBuilder { + change_policy: ChangeSpendPolicy::ChangeForbidden, + ..self.clone() + }) + } + + /// Only spend utxos added by [add_utxo]. The wallet will not add additional utxos to the transaction even if they are + /// needed to make the transaction valid. + fn manually_selected_only(&self) -> Arc<Self> { + Arc::new(TxBuilder { + manually_selected_only: true, + ..self.clone() + }) + } + + /// Only spend change outputs. This effectively adds all the non-change outputs to the "unspendable" list. See TxBuilder.unspendable. + fn only_spend_change(&self) -> Arc<Self> { + Arc::new(TxBuilder { + change_policy: ChangeSpendPolicy::OnlyChange, + ..self.clone() + }) + } + + /// Replace the internal list of unspendable utxos with a new list. It’s important to note that the "must-be-spent" utxos added with + /// TxBuilder.addUtxo have priority over these. See the Rust docs of the two linked methods for more details. + fn unspendable(&self, unspendable: Vec<OutPoint>) -> Arc<Self> { + Arc::new(TxBuilder { + unspendable: unspendable.into_iter().collect(), + ..self.clone() + }) + } + + /// Set a custom fee rate. + fn fee_rate(&self, sat_per_vb: f32) -> Arc<Self> { + Arc::new(TxBuilder { + fee_rate: Some(sat_per_vb), + ..self.clone() + }) + } + + /// Set an absolute fee. + fn fee_absolute(&self, fee_amount: u64) -> Arc<Self> { + Arc::new(TxBuilder { + fee_absolute: Some(fee_amount), + ..self.clone() + }) + } + + /// Spend all the available inputs. This respects filters like TxBuilder.unspendable and the change policy. + fn drain_wallet(&self) -> Arc<Self> { + Arc::new(TxBuilder { + drain_wallet: true, + ..self.clone() + }) + } + + /// Sets the address to drain excess coins to. Usually, when there are excess coins they are sent to a change address + /// generated by the wallet. This option replaces the usual change address with an arbitrary ScriptPubKey of your choosing. + /// Just as with a change output, if the drain output is not needed (the excess coins are too small) it will not be included + /// in the resulting transaction. The only difference is that it is valid to use drain_to without setting any ordinary recipients + /// with add_recipient (but it is perfectly fine to add recipients as well). If you choose not to set any recipients, you should + /// either provide the utxos that the transaction should spend via add_utxos, or set drain_wallet to spend all of them. + /// When bumping the fees of a transaction made with this option, you probably want to use BumpFeeTxBuilder.allow_shrinking + /// to allow this output to be reduced to pay for the extra fees. + fn drain_to(&self, address: String) -> Arc<Self> { + Arc::new(TxBuilder { + drain_to: Some(address), + ..self.clone() + }) + } + + /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. + fn enable_rbf(&self) -> Arc<Self> { + Arc::new(TxBuilder { + rbf: Some(RbfValue::Default), + ..self.clone() + }) + } + + /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an + /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD` + /// an error will be thrown, since it would not be a valid nSequence to signal RBF. + fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> { + Arc::new(TxBuilder { + rbf: Some(RbfValue::Value(nsequence)), + ..self.clone() + }) + } + + /// Add data as an output using OP_RETURN. + fn add_data(&self, data: Vec<u8>) -> Arc<Self> { + Arc::new(TxBuilder { + data, + ..self.clone() + }) + } + + /// Finish building the transaction. Returns the BIP174 PSBT. + fn finish(&self, wallet: &Wallet) -> Result<TxBuilderResult, BdkError> { + let wallet = wallet.get_wallet(); + let mut tx_builder = wallet.build_tx(); + for (script, amount) in &self.recipients { + tx_builder.add_recipient(script.clone(), *amount); + } + tx_builder.change_policy(self.change_policy); + if !self.utxos.is_empty() { + let bdk_utxos: Vec<BdkOutPoint> = self.utxos.iter().map(BdkOutPoint::from).collect(); + let utxos: &[BdkOutPoint] = &bdk_utxos; + tx_builder.add_utxos(utxos)?; + } + if !self.unspendable.is_empty() { + let bdk_unspendable: Vec<BdkOutPoint> = + self.unspendable.iter().map(BdkOutPoint::from).collect(); + tx_builder.unspendable(bdk_unspendable); + } + if self.manually_selected_only { + tx_builder.manually_selected_only(); + } + if let Some(sat_per_vb) = self.fee_rate { + tx_builder.fee_rate(FeeRate::from_sat_per_vb(sat_per_vb)); + } + if let Some(fee_amount) = self.fee_absolute { + tx_builder.fee_absolute(fee_amount); + } + if self.drain_wallet { + tx_builder.drain_wallet(); + } + if let Some(address) = &self.drain_to { + tx_builder.drain_to(to_script_pubkey(address)?); + } + if let Some(rbf) = &self.rbf { + match *rbf { + RbfValue::Default => { + tx_builder.enable_rbf(); + } + RbfValue::Value(nsequence) => { + tx_builder.enable_rbf_with_sequence(Sequence(nsequence)); + } + } + } + if !&self.data.is_empty() { + tx_builder.add_data(self.data.as_slice()); + } + + tx_builder + .finish() + .map(|(psbt, tx_details)| TxBuilderResult { + psbt: Arc::new(PartiallySignedTransaction { + internal: Mutex::new(psbt), + }), + transaction_details: TransactionDetails::from(&tx_details), + }) + } +} + +/// The BumpFeeTxBuilder is used to bump the fee on a transaction that has been broadcast and has its RBF flag set to true. +#[derive(Clone)] +struct BumpFeeTxBuilder { + txid: String, + fee_rate: f32, + allow_shrinking: Option<String>, + rbf: Option<RbfValue>, +} + +impl BumpFeeTxBuilder { + fn new(txid: String, fee_rate: f32) -> Self { + Self { + txid, + fee_rate, + allow_shrinking: None, + rbf: None, + } + } + + /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this script_pubkey + /// in order to bump the transaction fee. Without specifying this the wallet will attempt to find a change output to + /// shrink instead. Note that the output may shrink to below the dust limit and therefore be removed. If it is preserved + /// then it is currently not guaranteed to be in the same position as it was originally. Returns an error if script_pubkey + /// can’t be found among the recipients of the transaction we are bumping. + fn allow_shrinking(&self, address: String) -> Arc<Self> { + Arc::new(Self { + allow_shrinking: Some(address), + ..self.clone() + }) + } + + /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. + fn enable_rbf(&self) -> Arc<Self> { + Arc::new(Self { + rbf: Some(RbfValue::Default), + ..self.clone() + }) + } + + /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an + /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD` + /// an error will be thrown, since it would not be a valid nSequence to signal RBF. + fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> { + Arc::new(Self { + rbf: Some(RbfValue::Value(nsequence)), + ..self.clone() + }) + } + + /// Finish building the transaction. Returns the BIP174 PSBT. + fn finish(&self, wallet: &Wallet) -> Result<Arc<PartiallySignedTransaction>, BdkError> { + let wallet = wallet.get_wallet(); + let txid = Txid::from_str(self.txid.as_str())?; + let mut tx_builder = wallet.build_fee_bump(txid)?; + tx_builder.fee_rate(FeeRate::from_sat_per_vb(self.fee_rate)); + if let Some(allow_shrinking) = &self.allow_shrinking { + let address = BdkAddress::from_str(allow_shrinking) + .map_err(|e| BdkError::Generic(e.to_string()))?; + let script = address.script_pubkey(); + tx_builder.allow_shrinking(script)?; + } + if let Some(rbf) = &self.rbf { + match *rbf { + RbfValue::Default => { + tx_builder.enable_rbf(); + } + RbfValue::Value(nsequence) => { + tx_builder.enable_rbf_with_sequence(Sequence(nsequence)); + } + } + } + tx_builder + .finish() + .map(|(psbt, _)| PartiallySignedTransaction { + internal: Mutex::new(psbt), + }) + .map(Arc::new) + } +} + +/// Mnemonic phrases are a human-readable version of the private keys. +/// Supported number of words are 12, 15, 18, 21 and 24. +struct Mnemonic { + internal: BdkMnemonic, +} + +impl Mnemonic { + /// Generates Mnemonic with a random entropy + fn new(word_count: WordCount) -> Self { + let generated_key: GeneratedKey<_, BareCtx> = + BdkMnemonic::generate((word_count, Language::English)).unwrap(); + let mnemonic = BdkMnemonic::parse_in(Language::English, generated_key.to_string()).unwrap(); + Mnemonic { internal: mnemonic } + } + + /// Parse a Mnemonic with given string + fn from_string(mnemonic: String) -> Result<Self, BdkError> { + BdkMnemonic::from_str(&mnemonic) + .map(|m| Mnemonic { internal: m }) + .map_err(|e| BdkError::Generic(e.to_string())) + } + + /// Create a new Mnemonic in the specified language from the given entropy. + /// Entropy must be a multiple of 32 bits (4 bytes) and 128-256 bits in length. + fn from_entropy(entropy: Vec<u8>) -> Result<Self, BdkError> { + BdkMnemonic::from_entropy(entropy.as_slice()) + .map(|m| Mnemonic { internal: m }) + .map_err(|e| BdkError::Generic(e.to_string())) + } + + /// Returns Mnemonic as string + fn as_string(&self) -> String { + self.internal.to_string() + } +} + +struct DerivationPath { + derivation_path_mutex: Mutex<BdkDerivationPath>, +} + +impl DerivationPath { + fn new(path: String) -> Result<Self, BdkError> { + BdkDerivationPath::from_str(&path) + .map(|x| DerivationPath { + derivation_path_mutex: Mutex::new(x), + }) + .map_err(|e| BdkError::Generic(e.to_string())) + } +} + +struct DescriptorSecretKey { + descriptor_secret_key_mutex: Mutex<BdkDescriptorSecretKey>, +} + +impl DescriptorSecretKey { + fn new(network: Network, mnemonic: Arc<Mnemonic>, password: Option<String>) -> Self { + let mnemonic = mnemonic.internal.clone(); + let xkey: ExtendedKey = (mnemonic, password).into_extended_key().unwrap(); + let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { + origin: None, + xkey: xkey.into_xprv(network).unwrap(), + derivation_path: BdkDerivationPath::master(), + wildcard: bdk::descriptor::Wildcard::Unhardened, + }); + Self { + descriptor_secret_key_mutex: Mutex::new(descriptor_secret_key), + } + } + + fn derive(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> { + let secp = Secp256k1::new(); + let descriptor_secret_key = self.descriptor_secret_key_mutex.lock().unwrap(); + let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); + match descriptor_secret_key.deref() { + BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { + let derived_xprv = descriptor_x_key.xkey.derive_priv(&secp, &path)?; + let key_source = match descriptor_x_key.origin.clone() { + Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), + None => (descriptor_x_key.xkey.fingerprint(&secp), path), + }; + let derived_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { + origin: Some(key_source), + xkey: derived_xprv, + derivation_path: BdkDerivationPath::default(), + wildcard: descriptor_x_key.wildcard, + }); + Ok(Arc::new(Self { + descriptor_secret_key_mutex: Mutex::new(derived_descriptor_secret_key), + })) + } + BdkDescriptorSecretKey::Single(_) => { + unreachable!() + } + } + } + + fn extend(&self, path: Arc<DerivationPath>) -> Arc<Self> { + let descriptor_secret_key = self.descriptor_secret_key_mutex.lock().unwrap(); + let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); + match descriptor_secret_key.deref() { + BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { + let extended_path = descriptor_x_key.derivation_path.extend(path); + let extended_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { + origin: descriptor_x_key.origin.clone(), + xkey: descriptor_x_key.xkey, + derivation_path: extended_path, + wildcard: descriptor_x_key.wildcard, + }); + Arc::new(Self { + descriptor_secret_key_mutex: Mutex::new(extended_descriptor_secret_key), + }) + } + BdkDescriptorSecretKey::Single(_) => { + unreachable!() + } + } + } + + fn as_public(&self) -> Arc<DescriptorPublicKey> { + let secp = Secp256k1::new(); + let descriptor_public_key = self + .descriptor_secret_key_mutex + .lock() + .unwrap() + .to_public(&secp) + .unwrap(); + Arc::new(DescriptorPublicKey { + descriptor_public_key_mutex: Mutex::new(descriptor_public_key), + }) + } + + /// Get the private key as bytes. + fn secret_bytes(&self) -> Vec<u8> { + let descriptor_secret_key = self.descriptor_secret_key_mutex.lock().unwrap(); + let secret_bytes: Vec<u8> = match descriptor_secret_key.deref() { + BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { + descriptor_x_key.xkey.private_key.secret_bytes().to_vec() + } + BdkDescriptorSecretKey::Single(_) => { + unreachable!() + } + }; + + secret_bytes + } + + fn as_string(&self) -> String { + self.descriptor_secret_key_mutex.lock().unwrap().to_string() + } +} + +struct DescriptorPublicKey { + descriptor_public_key_mutex: Mutex<BdkDescriptorPublicKey>, +} + +impl DescriptorPublicKey { + fn derive(&self, path: Arc<DerivationPath>) -> Result<Arc<Self>, BdkError> { + let secp = Secp256k1::new(); + let descriptor_public_key = self.descriptor_public_key_mutex.lock().unwrap(); + let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); + + match descriptor_public_key.deref() { + BdkDescriptorPublicKey::XPub(descriptor_x_key) => { + let derived_xpub = descriptor_x_key.xkey.derive_pub(&secp, &path)?; + let key_source = match descriptor_x_key.origin.clone() { + Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), + None => (descriptor_x_key.xkey.fingerprint(), path), + }; + let derived_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey { + origin: Some(key_source), + xkey: derived_xpub, + derivation_path: BdkDerivationPath::default(), + wildcard: descriptor_x_key.wildcard, + }); + Ok(Arc::new(Self { + descriptor_public_key_mutex: Mutex::new(derived_descriptor_public_key), + })) + } + BdkDescriptorPublicKey::Single(_) => { + unreachable!() + } + } + } + + fn extend(&self, path: Arc<DerivationPath>) -> Arc<Self> { + let descriptor_public_key = self.descriptor_public_key_mutex.lock().unwrap(); + let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); + match descriptor_public_key.deref() { + BdkDescriptorPublicKey::XPub(descriptor_x_key) => { + let extended_path = descriptor_x_key.derivation_path.extend(path); + let extended_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey { + origin: descriptor_x_key.origin.clone(), + xkey: descriptor_x_key.xkey, + derivation_path: extended_path, + wildcard: descriptor_x_key.wildcard, + }); + Arc::new(Self { + descriptor_public_key_mutex: Mutex::new(extended_descriptor_public_key), + }) + } + BdkDescriptorPublicKey::Single(_) => { + unreachable!() + } + } + } + + fn as_string(&self) -> String { + self.descriptor_public_key_mutex.lock().unwrap().to_string() + } +} + +uniffi::deps::static_assertions::assert_impl_all!(Wallet: Sync, Send); + +// The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs. +// These tests should not be used to verify `bdk` behavior that is already tested in the `bdk` +// crate. +#[cfg(test)] +mod test { + use crate::*; + use bdk::bitcoin::Address; + use bdk::bitcoin::Network::Testnet; + use bdk::wallet::get_funded_wallet; + use std::str::FromStr; + use std::sync::Mutex; + + #[test] + fn test_drain_wallet() { + let test_wpkh = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; + let (funded_wallet, _, _) = get_funded_wallet(test_wpkh); + let test_wallet = Wallet { + wallet_mutex: Mutex::new(funded_wallet), + }; + let drain_to_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt".to_string(); + let tx_builder = TxBuilder::new() + .drain_wallet() + .drain_to(drain_to_address.clone()); + assert!(tx_builder.drain_wallet); + assert_eq!(tx_builder.drain_to, Some(drain_to_address)); + + let tx_builder_result = tx_builder.finish(&test_wallet).unwrap(); + let psbt = tx_builder_result.psbt.internal.lock().unwrap().clone(); + let tx_details = tx_builder_result.transaction_details; + + // confirm one input with 50,000 sats + assert_eq!(psbt.inputs.len(), 1); + let input_value = psbt + .inputs + .get(0) + .cloned() + .unwrap() + .non_witness_utxo + .unwrap() + .output + .get(0) + .unwrap() + .value; + assert_eq!(input_value, 50_000_u64); + + // confirm one output to correct address with all sats - fee + assert_eq!(psbt.outputs.len(), 1); + let output_address = Address::from_script( + &psbt + .unsigned_tx + .output + .get(0) + .cloned() + .unwrap() + .script_pubkey, + Testnet, + ) + .unwrap(); + assert_eq!( + output_address, + Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap() + ); + let output_value = psbt.unsigned_tx.output.get(0).cloned().unwrap().value; + assert_eq!(output_value, 49_890_u64); // input - fee + + assert_eq!( + tx_details.txid, + "312f1733badab22dc26b8dcbc83ba5629fb7b493af802e8abe07d865e49629c5" + ); + assert_eq!(tx_details.received, 0); + assert_eq!(tx_details.sent, 50000); + assert!(tx_details.fee.is_some()); + assert_eq!(tx_details.fee.unwrap(), 110); + assert!(tx_details.confirmation_time.is_none()); + } + + fn get_descriptor_secret_key() -> DescriptorSecretKey { + let mnemonic = Mnemonic::from_string("chaos fabric time speed sponsor all flat solution wisdom trophy crack object robot pave observe combine where aware bench orient secret primary cable detect".to_string()).unwrap(); + DescriptorSecretKey::new(Testnet, Arc::new(mnemonic), None) + } + + fn derive_dsk( + key: &DescriptorSecretKey, + path: &str, + ) -> Result<Arc<DescriptorSecretKey>, BdkError> { + let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); + key.derive(path) + } + + fn extend_dsk(key: &DescriptorSecretKey, path: &str) -> Arc<DescriptorSecretKey> { + let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); + key.extend(path) + } + + fn derive_dpk( + key: &DescriptorPublicKey, + path: &str, + ) -> Result<Arc<DescriptorPublicKey>, BdkError> { + let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); + key.derive(path) + } + + fn extend_dpk(key: &DescriptorPublicKey, path: &str) -> Arc<DescriptorPublicKey> { + let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); + key.extend(path) + } + + #[test] + fn test_generate_descriptor_secret_key() { + let master_dsk = get_descriptor_secret_key(); + assert_eq!(master_dsk.as_string(), "tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/*"); + assert_eq!(master_dsk.as_public().as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/*"); + } + + #[test] + fn test_derive_self() { + let master_dsk = get_descriptor_secret_key(); + let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m").unwrap(); + assert_eq!(derived_dsk.as_string(), "[d1d04177]tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/*"); + + let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); + let derived_dpk: &DescriptorPublicKey = &derive_dpk(master_dpk, "m").unwrap(); + assert_eq!(derived_dpk.as_string(), "[d1d04177]tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/*"); + } + + #[test] + fn test_derive_descriptors_keys() { + let master_dsk = get_descriptor_secret_key(); + let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m/0").unwrap(); + assert_eq!(derived_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/*"); + + let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); + let derived_dpk: &DescriptorPublicKey = &derive_dpk(master_dpk, "m/0").unwrap(); + assert_eq!(derived_dpk.as_string(), "[d1d04177/0]tpubD9oaCiP1MPmQdndm7DCD3D3QU34pWd6BbKSRedoZF1UJcNhEk3PJwkALNYkhxeTKL29oGNR7psqvT1KZydCGqUDEKXN6dVQJY2R8ooLPy8m/*"); + } + + #[test] + fn test_extend_descriptor_keys() { + let master_dsk = get_descriptor_secret_key(); + let extended_dsk: &DescriptorSecretKey = &extend_dsk(&master_dsk, "m/0"); + assert_eq!(extended_dsk.as_string(), "tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/0/*"); + + let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); + let extended_dpk: &DescriptorPublicKey = &extend_dpk(master_dpk, "m/0"); + assert_eq!(extended_dpk.as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/0/*"); + } + + #[test] + fn test_derive_and_extend_descriptor_secret_key() { + let master_dsk = get_descriptor_secret_key(); + // derive DescriptorSecretKey with path "m/0" from master + let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m/0").unwrap(); + assert_eq!(derived_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/*"); + + // extend derived_dsk with path "m/0" + let extended_dsk: &DescriptorSecretKey = &extend_dsk(derived_dsk, "m/0"); + assert_eq!(extended_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/0/*"); + } + + #[test] + fn test_derive_hardened_path_using_public() { + let master_dpk = get_descriptor_secret_key().as_public(); + let derived_dpk = &derive_dpk(&master_dpk, "m/84h/1h/0h"); + assert!(derived_dpk.is_err()); + } + + #[test] + fn test_retrieve_master_secret_key() { + let master_dpk = get_descriptor_secret_key(); + let master_private_key = master_dpk.secret_bytes().to_hex(); + assert_eq!( + master_private_key, + "e93315d6ce401eb4db803a56232f0ed3e69b053774e6047df54f1bd00e5ea936" + ) + } + + #[test] + fn test_psbt_fee() { + let test_wpkh = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; + let (funded_wallet, _, _) = get_funded_wallet(test_wpkh); + let test_wallet = Wallet { + wallet_mutex: Mutex::new(funded_wallet), + }; + let drain_to_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt".to_string(); + let tx_builder = TxBuilder::new() + .fee_rate(2.0) + .drain_wallet() + .drain_to(drain_to_address.clone()); + //dbg!(&tx_builder); + assert!(tx_builder.drain_wallet); + assert_eq!(tx_builder.drain_to, Some(drain_to_address)); + + let tx_builder_result = tx_builder.finish(&test_wallet).unwrap(); + + assert!(tx_builder_result.psbt.fee_rate().is_some()); + assert_eq!( + tx_builder_result.psbt.fee_rate().unwrap().as_sat_per_vb(), + 2.682927 + ); + + assert!(tx_builder_result.psbt.fee_amount().is_some()); + assert_eq!(tx_builder_result.psbt.fee_amount().unwrap(), 220); + } +} diff --git a/bdk-python/bdk-ffi/tests/README.md b/bdk-python/bdk-ffi/tests/README.md new file mode 100644 index 0000000..9d7c154 --- /dev/null +++ b/bdk-python/bdk-ffi/tests/README.md @@ -0,0 +1,21 @@ +# Integration tests for bdk-ffi + +This contains simple tests to make sure bdk-ffi can be used as a dependency for each of the +supported bindings languages. + +To skip integration tests and only run unit tests use `cargo test --lib`. + +To run all tests including integration tests use `CLASSPATH=./tests/jna/jna-5.8.0.jar cargo test`. + +Before running integration tests you must install the following development tools: + +1. [Java](https://openjdk.org/) and [Kotlin](https://kotlinlang.org/), +[sdkman](https://sdkman.io/) can help: + ```shell + sdk install java 11.0.16.1-zulu + sdk install kotlin 1.7.20` + ``` + +2. [Swift](https://www.swift.org/) + +3. [Python](https://www.python.org/) diff --git a/bdk-python/bdk-ffi/tests/bindings/test.kts b/bdk-python/bdk-ffi/tests/bindings/test.kts new file mode 100644 index 0000000..3ccac8f --- /dev/null +++ b/bdk-python/bdk-ffi/tests/bindings/test.kts @@ -0,0 +1,8 @@ +/* + * This is a basic test kotlin program that does nothing but confirm that the kotlin bindings compile + * and that a program that depends on them will run. + */ + +import org.bitcoindevkit.* + +val network = Network.TESTNET diff --git a/bdk-python/bdk-ffi/tests/bindings/test.py b/bdk-python/bdk-ffi/tests/bindings/test.py new file mode 100644 index 0000000..5b0c157 --- /dev/null +++ b/bdk-python/bdk-ffi/tests/bindings/test.py @@ -0,0 +1,15 @@ +import unittest +from bdk import * + +class TestBdk(unittest.TestCase): + + def test_some_enum(self): + network = Network.TESTNET + + def test_some_dict(self): + a = AddressInfo(index=42, address="testaddress") + self.assertEqual(42, a.index) + self.assertEqual("testaddress", a.address) + +if __name__=='__main__': + unittest.main() diff --git a/bdk-python/bdk-ffi/tests/bindings/test.swift b/bdk-python/bdk-ffi/tests/bindings/test.swift new file mode 100644 index 0000000..04afd6a --- /dev/null +++ b/bdk-python/bdk-ffi/tests/bindings/test.swift @@ -0,0 +1,9 @@ +/* + * This is a basic test swift program that does nothing but confirm that the swift bindings compile + * and that a program that depends on them will run. + */ + +import Foundation +import bdk + +let network = Network.testnet diff --git a/bdk-python/bdk-ffi/tests/jna/jna-5.8.0.jar b/bdk-python/bdk-ffi/tests/jna/jna-5.8.0.jar new file mode 100644 index 0000000..c3d534c Binary files /dev/null and b/bdk-python/bdk-ffi/tests/jna/jna-5.8.0.jar differ diff --git a/bdk-python/bdk-ffi/tests/test_generated_bindings.rs b/bdk-python/bdk-ffi/tests/test_generated_bindings.rs new file mode 100644 index 0000000..943c277 --- /dev/null +++ b/bdk-python/bdk-ffi/tests/test_generated_bindings.rs @@ -0,0 +1,8 @@ +uniffi_macros::build_foreign_language_testcases!( + ["src/bdk.udl",], + [ + "tests/bindings/test.kts", + "tests/bindings/test.swift", + "tests/bindings/test.py" + ] +); diff --git a/bdk-python/bdk-ffi/uniffi.toml b/bdk-python/bdk-ffi/uniffi.toml new file mode 100644 index 0000000..767e032 --- /dev/null +++ b/bdk-python/bdk-ffi/uniffi.toml @@ -0,0 +1,12 @@ +[bindings.kotlin] +package_name = "org.bitcoindevkit" +cdylib_name = "bdkffi" + +[bindings.python] +cdylib_name = "bdkffi" + +[bindings.ruby] +cdylib_name = "bdkffi" + +[bindings.swift] +cdylib_name = "bdkffi" diff --git a/bdk-python/generate.sh b/bdk-python/generate.sh new file mode 100644 index 0000000..fe1bd45 --- /dev/null +++ b/bdk-python/generate.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(dirname "$(realpath $0)") +PY_SRC="${SCRIPT_DIR}/src/bdkpython/" + +echo "Generating bdk.py..." +# GENERATE_PYTHON_BINDINGS_OUT="$PY_SRC" GENERATE_PYTHON_BINDINGS_FIXUP_LIB_PATH=bdkffi cargo run --manifest-path ./bdk-ffi/Cargo.toml --release --bin generate --features generate-python +# BDKFFI_BINDGEN_PYTHON_FIXUP_PATH=bdkffi cargo run --manifest-path ./bdk-ffi/Cargo.toml --package bdk-ffi-bindgen -- --language python --udl-file ./bdk-ffi/src/bdk.udl --out-dir ./src/bdkpython/ +BDKFFI_BINDGEN_OUTPUT_DIR="$PY_SRC" BDKFFI_BINDGEN_PYTHON_FIXUP_PATH=bdkffi cargo run --manifest-path ./bdk-ffi/Cargo.toml --package bdk-ffi-bindgen -- --language python --udl-file ./bdk-ffi/src/bdk.udl diff --git a/bdk-python/nix/uniffi_0.14.1_cargo_lock.patch b/bdk-python/nix/uniffi_0.14.1_cargo_lock.patch new file mode 100644 index 0000000..6c7244d --- /dev/null +++ b/bdk-python/nix/uniffi_0.14.1_cargo_lock.patch @@ -0,0 +1,387 @@ +--- /dev/null 2021-12-15 11:22:02.342000000 +0100 ++++ uniffi_bindgen/Cargo.lock 2021-12-15 16:15:16.132084011 +0100 +@@ -0,0 +1,384 @@ ++# This file is automatically @generated by Cargo. ++# It is not intended for manual editing. ++version = 3 ++ ++[[package]] ++name = "anyhow" ++version = "1.0.51" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" ++ ++[[package]] ++name = "arrayvec" ++version = "0.5.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" ++ ++[[package]] ++name = "askama" ++version = "0.10.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" ++dependencies = [ ++ "askama_derive", ++ "askama_escape", ++ "askama_shared", ++] ++ ++[[package]] ++name = "askama_derive" ++version = "0.10.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" ++dependencies = [ ++ "askama_shared", ++ "proc-macro2", ++ "syn", ++] ++ ++[[package]] ++name = "askama_escape" ++version = "0.10.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" ++ ++[[package]] ++name = "askama_shared" ++version = "0.11.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc" ++dependencies = [ ++ "askama_escape", ++ "nom 6.2.1", ++ "proc-macro2", ++ "quote", ++ "serde", ++ "syn", ++ "toml", ++] ++ ++[[package]] ++name = "bitflags" ++version = "1.3.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" ++ ++[[package]] ++name = "bitvec" ++version = "0.19.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" ++dependencies = [ ++ "funty", ++ "radium", ++ "tap", ++ "wyz", ++] ++ ++[[package]] ++name = "camino" ++version = "1.0.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "cargo-platform" ++version = "0.1.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "cargo_metadata" ++version = "0.13.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "081e3f0755c1f380c2d010481b6fa2e02973586d5f2b24eebb7a2a1d98b143d8" ++dependencies = [ ++ "camino", ++ "cargo-platform", ++ "semver", ++ "semver-parser", ++ "serde", ++ "serde_json", ++] ++ ++[[package]] ++name = "cfg-if" ++version = "1.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" ++ ++[[package]] ++name = "clap" ++version = "2.34.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" ++dependencies = [ ++ "bitflags", ++ "textwrap", ++ "unicode-width", ++] ++ ++[[package]] ++name = "funty" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" ++ ++[[package]] ++name = "heck" ++version = "0.3.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" ++dependencies = [ ++ "unicode-segmentation", ++] ++ ++[[package]] ++name = "itoa" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" ++ ++[[package]] ++name = "lexical-core" ++version = "0.7.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" ++dependencies = [ ++ "arrayvec", ++ "bitflags", ++ "cfg-if", ++ "ryu", ++ "static_assertions", ++] ++ ++[[package]] ++name = "memchr" ++version = "2.3.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" ++ ++[[package]] ++name = "nom" ++version = "5.1.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" ++dependencies = [ ++ "memchr", ++ "version_check", ++] ++ ++[[package]] ++name = "nom" ++version = "6.2.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" ++dependencies = [ ++ "bitvec", ++ "funty", ++ "lexical-core", ++ "memchr", ++ "version_check", ++] ++ ++[[package]] ++name = "paste" ++version = "1.0.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" ++ ++[[package]] ++name = "pest" ++version = "2.1.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" ++dependencies = [ ++ "ucd-trie", ++] ++ ++[[package]] ++name = "proc-macro2" ++version = "1.0.34" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" ++dependencies = [ ++ "unicode-xid", ++] ++ ++[[package]] ++name = "quote" ++version = "1.0.10" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" ++dependencies = [ ++ "proc-macro2", ++] ++ ++[[package]] ++name = "radium" ++version = "0.5.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" ++ ++[[package]] ++name = "ryu" ++version = "1.0.9" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" ++ ++[[package]] ++name = "semver" ++version = "0.11.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" ++dependencies = [ ++ "semver-parser", ++ "serde", ++] ++ ++[[package]] ++name = "semver-parser" ++version = "0.10.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" ++dependencies = [ ++ "pest", ++] ++ ++[[package]] ++name = "serde" ++version = "1.0.131" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" ++dependencies = [ ++ "serde_derive", ++] ++ ++[[package]] ++name = "serde_derive" ++version = "1.0.131" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "serde_json" ++version = "1.0.73" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" ++dependencies = [ ++ "itoa", ++ "ryu", ++ "serde", ++] ++ ++[[package]] ++name = "static_assertions" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" ++ ++[[package]] ++name = "syn" ++version = "1.0.82" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "unicode-xid", ++] ++ ++[[package]] ++name = "tap" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" ++ ++[[package]] ++name = "textwrap" ++version = "0.11.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" ++dependencies = [ ++ "unicode-width", ++] ++ ++[[package]] ++name = "toml" ++version = "0.5.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "ucd-trie" ++version = "0.1.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" ++ ++[[package]] ++name = "unicode-segmentation" ++version = "1.8.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" ++ ++[[package]] ++name = "unicode-width" ++version = "0.1.9" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" ++ ++[[package]] ++name = "unicode-xid" ++version = "0.2.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" ++ ++[[package]] ++name = "uniffi_bindgen" ++version = "0.14.1" ++dependencies = [ ++ "anyhow", ++ "askama", ++ "cargo_metadata", ++ "clap", ++ "heck", ++ "paste", ++ "serde", ++ "toml", ++ "weedle", ++] ++ ++[[package]] ++name = "version_check" ++version = "0.9.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" ++ ++[[package]] ++name = "weedle" ++version = "0.12.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "610950904727748ca09682e857f0d6d6437f0ca862f32f9229edba8cec8b2635" ++dependencies = [ ++ "nom 5.1.2", ++] ++ ++[[package]] ++name = "wyz" ++version = "0.2.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/bdk-python/nix/uniffi_0.15.2_cargo_lock.patch b/bdk-python/nix/uniffi_0.15.2_cargo_lock.patch new file mode 100644 index 0000000..7fbc9e6 --- /dev/null +++ b/bdk-python/nix/uniffi_0.15.2_cargo_lock.patch @@ -0,0 +1,387 @@ +--- /dev/null 2021-12-15 11:22:02.342000000 +0100 ++++ uniffi_bindgen/Cargo.lock 2021-12-15 15:54:49.278543090 +0100 +@@ -0,0 +1,384 @@ ++# This file is automatically @generated by Cargo. ++# It is not intended for manual editing. ++version = 3 ++ ++[[package]] ++name = "anyhow" ++version = "1.0.51" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" ++ ++[[package]] ++name = "arrayvec" ++version = "0.5.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" ++ ++[[package]] ++name = "askama" ++version = "0.10.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" ++dependencies = [ ++ "askama_derive", ++ "askama_escape", ++ "askama_shared", ++] ++ ++[[package]] ++name = "askama_derive" ++version = "0.10.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" ++dependencies = [ ++ "askama_shared", ++ "proc-macro2", ++ "syn", ++] ++ ++[[package]] ++name = "askama_escape" ++version = "0.10.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" ++ ++[[package]] ++name = "askama_shared" ++version = "0.11.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc" ++dependencies = [ ++ "askama_escape", ++ "nom 6.2.1", ++ "proc-macro2", ++ "quote", ++ "serde", ++ "syn", ++ "toml", ++] ++ ++[[package]] ++name = "bitflags" ++version = "1.3.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" ++ ++[[package]] ++name = "bitvec" ++version = "0.19.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" ++dependencies = [ ++ "funty", ++ "radium", ++ "tap", ++ "wyz", ++] ++ ++[[package]] ++name = "camino" ++version = "1.0.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "cargo-platform" ++version = "0.1.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "cargo_metadata" ++version = "0.13.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "081e3f0755c1f380c2d010481b6fa2e02973586d5f2b24eebb7a2a1d98b143d8" ++dependencies = [ ++ "camino", ++ "cargo-platform", ++ "semver", ++ "semver-parser", ++ "serde", ++ "serde_json", ++] ++ ++[[package]] ++name = "cfg-if" ++version = "1.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" ++ ++[[package]] ++name = "clap" ++version = "2.34.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" ++dependencies = [ ++ "bitflags", ++ "textwrap", ++ "unicode-width", ++] ++ ++[[package]] ++name = "funty" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" ++ ++[[package]] ++name = "heck" ++version = "0.3.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" ++dependencies = [ ++ "unicode-segmentation", ++] ++ ++[[package]] ++name = "itoa" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" ++ ++[[package]] ++name = "lexical-core" ++version = "0.7.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" ++dependencies = [ ++ "arrayvec", ++ "bitflags", ++ "cfg-if", ++ "ryu", ++ "static_assertions", ++] ++ ++[[package]] ++name = "memchr" ++version = "2.3.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" ++ ++[[package]] ++name = "nom" ++version = "5.1.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" ++dependencies = [ ++ "memchr", ++ "version_check", ++] ++ ++[[package]] ++name = "nom" ++version = "6.2.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" ++dependencies = [ ++ "bitvec", ++ "funty", ++ "lexical-core", ++ "memchr", ++ "version_check", ++] ++ ++[[package]] ++name = "paste" ++version = "1.0.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" ++ ++[[package]] ++name = "pest" ++version = "2.1.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" ++dependencies = [ ++ "ucd-trie", ++] ++ ++[[package]] ++name = "proc-macro2" ++version = "1.0.34" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" ++dependencies = [ ++ "unicode-xid", ++] ++ ++[[package]] ++name = "quote" ++version = "1.0.10" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" ++dependencies = [ ++ "proc-macro2", ++] ++ ++[[package]] ++name = "radium" ++version = "0.5.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" ++ ++[[package]] ++name = "ryu" ++version = "1.0.9" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" ++ ++[[package]] ++name = "semver" ++version = "0.11.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" ++dependencies = [ ++ "semver-parser", ++ "serde", ++] ++ ++[[package]] ++name = "semver-parser" ++version = "0.10.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" ++dependencies = [ ++ "pest", ++] ++ ++[[package]] ++name = "serde" ++version = "1.0.131" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" ++dependencies = [ ++ "serde_derive", ++] ++ ++[[package]] ++name = "serde_derive" ++version = "1.0.131" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "serde_json" ++version = "1.0.73" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" ++dependencies = [ ++ "itoa", ++ "ryu", ++ "serde", ++] ++ ++[[package]] ++name = "static_assertions" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" ++ ++[[package]] ++name = "syn" ++version = "1.0.82" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "unicode-xid", ++] ++ ++[[package]] ++name = "tap" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" ++ ++[[package]] ++name = "textwrap" ++version = "0.11.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" ++dependencies = [ ++ "unicode-width", ++] ++ ++[[package]] ++name = "toml" ++version = "0.5.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" ++dependencies = [ ++ "serde", ++] ++ ++[[package]] ++name = "ucd-trie" ++version = "0.1.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" ++ ++[[package]] ++name = "unicode-segmentation" ++version = "1.8.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" ++ ++[[package]] ++name = "unicode-width" ++version = "0.1.9" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" ++ ++[[package]] ++name = "unicode-xid" ++version = "0.2.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" ++ ++[[package]] ++name = "uniffi_bindgen" ++version = "0.15.2" ++dependencies = [ ++ "anyhow", ++ "askama", ++ "cargo_metadata", ++ "clap", ++ "heck", ++ "paste", ++ "serde", ++ "toml", ++ "weedle", ++] ++ ++[[package]] ++name = "version_check" ++version = "0.9.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" ++ ++[[package]] ++name = "weedle" ++version = "0.12.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "610950904727748ca09682e857f0d6d6437f0ca862f32f9229edba8cec8b2635" ++dependencies = [ ++ "nom 5.1.2", ++] ++ ++[[package]] ++name = "wyz" ++version = "0.2.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/bdk-python/nix/uniffi_bindgen.nix b/bdk-python/nix/uniffi_bindgen.nix new file mode 100644 index 0000000..42cf6ab --- /dev/null +++ b/bdk-python/nix/uniffi_bindgen.nix @@ -0,0 +1,20 @@ +with import <nixpkgs> {}; + +rustPlatform.buildRustPackage rec { + pname = "uniffi_bindgen"; + version = "0.15.2"; + src = fetchFromGitHub { + owner = "mozilla"; + repo = "uniffi-rs"; + rev = "6fa9c06a394b4e9b219fa30fc94e353d17f86e11"; + # rev = "refs/tags/v0.14.1"; + sha256 = "1chahy1ac1r88drpslln2p1b04cbg79ylpxzyyp92s1z7ldm5ddb"; # 0.15.2 + # sha256 = "1mff3f3fqqzqx1yv70ff1yzdnvbd90vg2r477mzzcgisg1wfpwi0"; # 0.14.1 + fetchSubmodules = true; + } + "/uniffi_bindgen/"; + + doCheck = false; + cargoSha256 = "sha256:08gg285fq8i32nf9kd8s0nn0niacd7sg8krv818nx41i18sm2cf3"; # 0.15.2 + # cargoSha256 = "sha256:01zp3rwlni988h02dqhkhzhwccs7bhwc1alhbf6gbw3av4b0m9cf"; # 0.14.1 + cargoPatches = [ ./uniffi_0.15.2_cargo_lock.patch ]; +} diff --git a/bdk-python/pyproject.toml b/bdk-python/pyproject.toml new file mode 100644 index 0000000..2012f16 --- /dev/null +++ b/bdk-python/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools-rust"] + +[tool.pytest.ini_options] +pythonpath = [ + "." +] \ No newline at end of file diff --git a/bdk-python/requirements-dev.txt b/bdk-python/requirements-dev.txt new file mode 100644 index 0000000..b95a8a8 --- /dev/null +++ b/bdk-python/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest==7.1.2 +tox==3.25.1 diff --git a/bdk-python/requirements.txt b/bdk-python/requirements.txt new file mode 100644 index 0000000..d6ba728 --- /dev/null +++ b/bdk-python/requirements.txt @@ -0,0 +1,3 @@ +semantic-version==2.9.0 +setuptools-rust==1.1.2 +typing_extensions==4.0.1 diff --git a/bdk-python/setup.py b/bdk-python/setup.py new file mode 100644 index 0000000..8462a6a --- /dev/null +++ b/bdk-python/setup.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import os + +from setuptools import setup +from setuptools_rust import Binding, RustExtension + +LONG_DESCRIPTION = """# bdkpython +The Python language bindings for the [Bitcoin Dev Kit](https://github.com/bitcoindevkit). + +## Install the package +```shell +pip install bdkpython +``` + +## Simple example +```python +import bdkpython as bdk + + +descriptor = "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/84h/0h/0h/0/*)" +db_config = bdk.DatabaseConfig.MEMORY() +blockchain_config = bdk.BlockchainConfig.ELECTRUM( + bdk.ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + None, + 5, + None, + 100 + ) +) +blockchain = bdk.Blockchain(blockchain_config) + +wallet = bdk.Wallet( + descriptor=descriptor, + change_descriptor=None, + network=bdk.Network.TESTNET, + database_config=db_config, + ) + +# print new receive address +address_info = wallet.get_address(bdk.AddressIndex.LAST_UNUSED) +address = address_info.address +index = address_info.index +print(f"New BIP84 testnet address: {address} at index {index}") + + +# print wallet balance +wallet.sync(blockchain, None) +balance = wallet.get_balance() +print(f"Wallet balance is: {balance.total}") +""" + +rust_ext = RustExtension( + target="bdkpython.bdkffi", + path="./bdk-ffi/Cargo.toml", + binding=Binding.NoBinding, +) + +setup( + name='bdkpython', + version='0.6.0.dev0', + description="The Python language bindings for the Bitcoin Development Kit", + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + rust_extensions=[rust_ext], + zip_safe=False, + packages=['bdkpython'], + package_dir={'bdkpython': './src/bdkpython'}, + url="https://github.com/bitcoindevkit/bdk-python", + author="Alekos Filini <alekos.filini@gmail.com>, Steve Myers <steve@notmandatory.org>", + license="MIT or Apache 2.0", +) diff --git a/bdk-python/shell.nix b/bdk-python/shell.nix new file mode 100644 index 0000000..1b59fca --- /dev/null +++ b/bdk-python/shell.nix @@ -0,0 +1,17 @@ +with import <nixpkgs> {}; + +mkShell { + name = "bdk-python-shell"; + packages = [ ( import ./nix/uniffi_bindgen.nix ) ]; + buildInputs = with python37.pkgs; [ + pip + setuptools + ]; + shellHook = '' + export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH + alias pip="PIP_PREFIX='$(pwd)/_build/pip_packages' \pip" + export PYTHONPATH="$(pwd)/_build/pip_packages/lib/python3.7/site-packages:$(pwd):$PYTHONPATH" + export PATH="$(pwd)/_build/pip_packages/bin:$PATH" + unset SOURCE_DATE_EPOCH + ''; +} diff --git a/bdk-python/src/bdkpython/__init__.py b/bdk-python/src/bdkpython/__init__.py new file mode 100644 index 0000000..0b0ba05 --- /dev/null +++ b/bdk-python/src/bdkpython/__init__.py @@ -0,0 +1 @@ +from bdkpython.bdk import * diff --git a/bdk-python/tests/test_bdk.py b/bdk-python/tests/test_bdk.py new file mode 100644 index 0000000..e546592 --- /dev/null +++ b/bdk-python/tests/test_bdk.py @@ -0,0 +1,46 @@ +import bdkpython as bdk +import unittest + +descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)" +db_config = bdk.DatabaseConfig.MEMORY() +blockchain_config = bdk.BlockchainConfig.ELECTRUM( + bdk.ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + None, + 5, + None, + 100 + ) +) +blockchain = bdk.Blockchain(blockchain_config) + + +class TestSimpleBip84Wallet(unittest.TestCase): + + def test_address_bip84_testnet(self): + wallet = bdk.Wallet( + descriptor=descriptor, + change_descriptor=None, + network=bdk.Network.TESTNET, + database_config=db_config + ) + address_info = wallet.get_address(bdk.AddressIndex.LAST_UNUSED) + address = address_info.address + # print(f"New address is {address}") + assert address == "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", f"Wrong address {address}, should be tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e" + + def test_wallet_balance(self): + wallet = bdk.Wallet( + descriptor=descriptor, + change_descriptor=None, + network=bdk.Network.TESTNET, + database_config=db_config, + ) + wallet.sync(blockchain, None) + balance = wallet.get_balance() + # print(f"Balance is {balance.total} sat") + assert balance.total > 0, "Balance is 0, send testnet coins to tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e" + + +if __name__ == '__main__': + unittest.main() diff --git a/bdk-python/tox.ini b/bdk-python/tox.ini new file mode 100644 index 0000000..c01a149 --- /dev/null +++ b/bdk-python/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = + py38 + py39 + + +[testenv] +usedevelop=true +deps = + -rrequirements.txt + -rrequirements-dev.txt +commands = + python3 setup.py -v build + python3 setup.py -v install + pytest --verbose --override-ini console_output_style=count + python3 setup.py --verbose bdist_wheel