diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..65cdb95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: Bug Report +description: Create a report to help us improve +labels: [ "bug", "triage" ] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: description + attributes: + label: Description + description: A concise description of the bug, what happened? + placeholder: What did you see... + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How to Reproduce + description: Steps or code to reproduce the behavior. + placeholder: How can we reproduce it... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Result + description: What did you expected to happen. + placeholder: Tell us what you were expecting... + validations: + required: true + - type: textarea + id: version + attributes: + label: Version + description: Which release version(s), commit, or branch of code do you see this bug? + placeholder: Tell us which version(s) of code you saw the bug in... + validations: + required: true + - type: checkboxes + id: artifact + attributes: + label: Artifact + description: With which artifact(s) are you seeing this bug? + options: + - label: bdk-jvm + - label: bdk-android + - type: checkboxes + id: platform + attributes: + label: Platform + description: What target platform(s) are you seeing the problem on? + options: + - label: ARM64 Android + - label: 64-bit x86 Android + - label: 32-bit x86 Android + - label: 64-bit x86 Linux (kernel 2.6.32+, glibc 2.11+) + - label: 64-bit x86 macOS (10.7+, Lion+) + - label: ARM64 macOS (11.0+, Big Sur+) + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9e5f358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: 📝 Official Documentation + url: https://bitcoindevkit.org/getting-started/ + about: Check our documentation for answers to common questions + - name: 💬 Community Chat + url: https://discord.com/invite/dstn4dQ + about: Ask general questions and get community support in real-time diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..cd16c23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature Request +description: Request a new feature +labels: [ "enhancement", "triage" ] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to request and document this new feature! + - type: textarea + id: problem + attributes: + label: Problem + description: A concise description of the problem you need this new feature to solve. + placeholder: What is the problem you are trying to solve... + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A concise description of the new feature you need. + placeholder: What will this new feature do, how will it work... + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Describe any other alternatives you considered. + placeholder: Other ways you considered to solve your problem... + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 42afd7e..947a092 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - + ### Description diff --git a/.github/workflows/publish-android.yaml b/.github/workflows/publish-android.yaml new file mode 100644 index 0000000..bb86761 --- /dev/null +++ b/.github/workflows/publish-android.yaml @@ -0,0 +1,60 @@ +name: Publish bdk-android to Maven Central +on: [workflow_dispatch] + +env: + ANDROID_NDK_ROOT: /usr/local/lib/android/sdk/ndk/21.4.7075529 + # By default the new ubuntu-20.04 images use the following ANDROID_NDK_ROOT + # ANDROID_NDK_ROOT: /usr/local/lib/android/sdk/ndk/25.0.8775105 + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: Install Android NDK 21.4.7075529 + run: | + ANDROID_ROOT=/usr/local/lib/android + ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk + SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager + echo "y" | $SDKMANAGER "ndk;21.4.7075529" + + - name: Check out PR branch + uses: actions/checkout@v2 + + - name: Update bdk-ffi git submodule + run: | + git submodule set-url bdk-ffi https://github.com/bitcoindevkit/bdk-ffi.git + git submodule update --init bdk-ffi + + - name: cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + bdk-ffi/target + key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Install rust android targets + run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi + + - name: Build bdk-android library + run: | + cd bdk-android + ./gradlew buildAndroidLib + + - name: Publish to Maven Local and Maven Central + env: + ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} + ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} + run: | + cd bdk-android + ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/publish-jvm.yaml b/.github/workflows/publish-jvm.yaml new file mode 100644 index 0000000..19668b3 --- /dev/null +++ b/.github/workflows/publish-jvm.yaml @@ -0,0 +1,101 @@ +name: Publish bdk-jvm to Maven Central +on: [workflow_dispatch] +#on: +# release: +# types: [published] + +jobs: + build-jvm-macOS-M1-native-lib: + name: Create M1 and x86_64 JVM native binaries + runs-on: macos-12 + steps: + - name: Checkout publishing branch + uses: actions/checkout@v2 + + - name: Update bdk-ffi git submodule + run: | + git submodule set-url bdk-ffi https://github.com/bitcoindevkit/bdk-ffi.git + git submodule update --init bdk-ffi + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ./bdk-ffi/target + key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Install aarch64 Rust target + run: rustup target add aarch64-apple-darwin + + - name: Build bdk-jvm library + run: | + cd bdk-jvm + ./gradlew buildJvmLib + + # build aarch64 + x86_64 native libraries and upload + - name: Upload macOS native libraries for reuse in publishing job + uses: actions/upload-artifact@v3 + with: + # name: no name is required because we upload the entire directory + # the default name "artifact" will be used + path: /Users/runner/work/bdk-kotlin/bdk-kotlin/bdk-jvm/lib/src/main/resources/ + + build-jvm-full-library: + name: Create full bdk-jvm library + needs: [build-jvm-macOS-M1-native-lib] + runs-on: ubuntu-22.04 + steps: + - name: Checkout publishing branch + uses: actions/checkout@v2 + + - name: Update bdk-ffi git submodule + run: | + git submodule set-url bdk-ffi https://github.com/bitcoindevkit/bdk-ffi.git + git submodule update --init bdk-ffi + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ./bdk-ffi/target + key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Build bdk-jvm library + run: | + cd bdk-jvm + ./gradlew buildJvmLib + + - name: Download macOS native libraries from previous job + uses: actions/download-artifact@v3 + id: download + with: + # download the artifact created in the prior job (named "artifact") + name: artifact + path: ./bdk-jvm/lib/src/main/resources/ + + - name: Publish to Maven Central + env: + ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} + ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} + run: | + cd bdk-jvm + ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/test-android.yaml b/.github/workflows/test-android.yaml new file mode 100644 index 0000000..2b3f223 --- /dev/null +++ b/.github/workflows/test-android.yaml @@ -0,0 +1,54 @@ +name: Test Android +on: [push, pull_request] + +env: + ANDROID_NDK_ROOT: /usr/local/lib/android/sdk/ndk/21.4.7075529 + # By default the new ubuntu-20.04 images use the following ANDROID_NDK_ROOT + # ANDROID_NDK_ROOT: /usr/local/lib/android/sdk/ndk/25.0.8775105 + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: Install Android NDK 21.4.7075529 + run: | + ANDROID_ROOT=/usr/local/lib/android + ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk + SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager + echo "y" | $SDKMANAGER "ndk;21.4.7075529" + + - name: Check out PR branch + uses: actions/checkout@v2 + + - name: Update bdk-ffi git submodule + run: | + git submodule set-url bdk-ffi https://github.com/bitcoindevkit/bdk-ffi.git + git submodule update --init bdk-ffi + + - name: cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + bdk-ffi/target + key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Install rust android targets + run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi + + - name: Build bdk-android library + run: | + cd bdk-android + ./gradlew buildAndroidLib + + - name: Run Android tests + run: | + cd bdk-android + ./gradlew test --console=rich diff --git a/.github/workflows/test-jvm.yaml b/.github/workflows/test-jvm.yaml new file mode 100644 index 0000000..c9f5a4a --- /dev/null +++ b/.github/workflows/test-jvm.yaml @@ -0,0 +1,39 @@ +name: Test JVM +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: Check out PR branch + uses: actions/checkout@v2 + + - name: Update bdk-ffi git submodule + run: | + git submodule set-url bdk-ffi https://github.com/bitcoindevkit/bdk-ffi.git + git submodule update --init bdk-ffi + + - name: cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + bdk-ffi/target + key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Build bdk-jvm library + run: | + cd bdk-jvm + ./gradlew buildJvmLib + + - name: Run JVM tests + run: | + cd bdk-jvm + ./gradlew test --console=rich diff --git a/bdk-kotlin/.editorconfig b/bdk-kotlin/.editorconfig new file mode 100644 index 0000000..4f1c5cd --- /dev/null +++ b/bdk-kotlin/.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-kotlin/.gitignore b/bdk-kotlin/.gitignore new file mode 100644 index 0000000..7b20211 --- /dev/null +++ b/bdk-kotlin/.gitignore @@ -0,0 +1,19 @@ +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 +bdk-android/lib/src/main/kotlin/org/bitcoindevkit/bdk.kt +bdk-jvm/lib/src/main/kotlin/org/bitcoindevkit/bdk.kt diff --git a/bdk-kotlin/.gitmodules b/bdk-kotlin/.gitmodules new file mode 100644 index 0000000..c582725 --- /dev/null +++ b/bdk-kotlin/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bdk-ffi"] + path = bdk-ffi + url = https://github.com/bitcoindevkit/bdk-ffi.git diff --git a/bdk-kotlin/LICENSE b/bdk-kotlin/LICENSE new file mode 100644 index 0000000..9c61848 --- /dev/null +++ b/bdk-kotlin/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 or the MIT license , 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-kotlin/LICENSE-APACHE b/bdk-kotlin/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/bdk-kotlin/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-kotlin/LICENSE-MIT b/bdk-kotlin/LICENSE-MIT new file mode 100644 index 0000000..9d982a4 --- /dev/null +++ b/bdk-kotlin/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-kotlin/PGP-BDK-BINDINGS.asc b/bdk-kotlin/PGP-BDK-BINDINGS.asc new file mode 100644 index 0000000..6b5af63 --- /dev/null +++ b/bdk-kotlin/PGP-BDK-BINDINGS.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYw6xkRYJKwYBBAHaRw8BAQdAg+VLXuidDqeP015H/QMlESJyQeIntTUoQkbk ++IFu+jO0M2JpdGNvaW5kZXZraXQtYmluZGluZ3MgPGJpbmRpbmdzQGJpdGNvaW5k +ZXZraXQub3JnPoiTBBMWCgA7FiEEiK2TrEWJ/QkP87jRJ2jEPogDxqMFAmMOsZEC +GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQJ2jEPogDxqPQTgEA292D +RQaxDTJ4k91D0w50Vrd0NSNUwlsERz9XJ64abWABAP99vGMmq2pfrngTQqjLgLe8 +0YhQ+VML2x/B0LSN6MgNuDgEYw6xkRIKKwYBBAGXVQEFAQEHQEkUJv+/Wzx7nNiX +eti3HkeT6ZNAuCExPE4F7jxHNQ1TAwEIB4h4BBgWCgAgFiEEiK2TrEWJ/QkP87jR +J2jEPogDxqMFAmMOsZECGwwACgkQJ2jEPogDxqObPQEA/B0xNew03KM0JP630efG +QT/3Caq/jx86pLwnB7XqWI8BAOKmqrOEiwCBjhaIpzC3/1M+aZuPRUL3V91uPxpM +jFAJ +=vvmK +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/bdk-kotlin/README.md b/bdk-kotlin/README.md new file mode 100644 index 0000000..297d48f --- /dev/null +++ b/bdk-kotlin/README.md @@ -0,0 +1,169 @@ +# bdk-kotlin + +This project builds .jar and .aar packages for the `jvm` and `android` platforms that provide +[Kotlin] language bindings for the [`bdk`] library. The Kotlin language bindings are created by the +[`bdk-ffi`] project which is included as a git submodule of this repository. + +## How to Use + +To use the Kotlin language bindings for [`bdk`] in your `jvm` or `android` project add the +following to your gradle dependencies: +```groovy +repositories { + mavenCentral() +} + +dependencies { + + // for jvm + implementation 'org.bitcoindevkit:bdk-jvm:' + // OR for android + implementation 'org.bitcoindevkit:bdk-android:' + +} +``` + +You may then import and use the `org.bitcoindevkit` library in your Kotlin code. For example: + +```kotlin +import org.bitcoindevkit.* + +// ... + +val externalDescriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)" +val internalDescriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)" + +val databaseConfig = DatabaseConfig.Memory + +val blockchainConfig = + BlockchainConfig.Electrum( + ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5u, null, 10u) + ) +val wallet = Wallet(externalDescriptor, internalDescriptor, Network.TESTNET, databaseConfig, blockchainConfig) +val newAddress = wallet.getNewAddress() +``` + +### Example Projects + +#### `bdk-android` +* [Devkit Wallet](https://github.com/thunderbiscuit/devkit-wallet) +* [Padawan Wallet](https://github.com/thunderbiscuit/padawan-wallet) + +#### `bdk-jvm` +* [Tatooine Faucet](https://github.com/thunderbiscuit/tatooine) + +### How to build +_Note that Kotlin version `1.6.10` or later is required to build the library._ + +1. Clone this repository and initialize and update its [`bdk-ffi`] submodule. +```shell +git clone https://github.com/bitcoindevkit/bdk-kotlin +git submodule update --init +``` +2. Follow the "General" bdk-ffi ["Getting Started (Developer)"] instructions. +3. If building on MacOS install required intel and m1 jvm targets +```sh +rustup target add x86_64-apple-darwin aarch64-apple-darwin +``` +4. Install required targets + ```sh + rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi + ``` +5. Install Android SDK and Build-Tools for API level 30+ +6. Setup `$ANDROID_SDK_ROOT` and `$ANDROID_NDK_ROOT` path variables (which are required by the + build tool), for example (NDK major version 21 is required): + ```shell + export ANDROID_SDK_ROOT=~/Android/Sdk + export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/21. + ``` +7. Build kotlin bindings + ```sh + # build JVM library + cd bdk-jvm + ./gradlew buildJvmLib + + # build Android library + cd bdk-android + ./gradlew buildAndroidLib + ``` +8. Start android emulator (must be x86_64) and run tests +```sh +./gradlew connectedAndroidTest +``` + +## How to publish + +### Publish to your local maven repo +```shell +# bdk-jvm +cd bdk-jvm +./gradlew publishToMavenLocal --exclude-task signMavenPublication + +# bdk-android +cd bdk-android +./gradlew publishToMavenLocal --exclude-task signMavenPublication +``` + +Note that the commands assume you don't need the local libraries to be signed. If you do wish to sign them, simply set your `~/.gradle/gradle.properties` signing key values like so: +```properties +signing.gnupg.keyName= +signing.gnupg.passphrase= +``` + +and use the `publishToMavenLocal` task without excluding the signing task: +```shell +./gradlew publishToMavenLocal +``` + +## Verifying Signatures +Both libraries and all their corresponding artifacts are signed with a PGP key you can find in the +root of this repository. To verify the signatures follow the below steps: + +1. Import the PGP key in your keyring. +```shell +# Navigate to the root of the repository and import the ./PGP-BDK-BINDINGS.asc public key +gpg --import ./PGP-BDK-BINDINGS.asc + +# Alternatively, you can import the key directly from a public key server +gpg --keyserver keyserver.ubuntu.com --receive-key 2768C43E8803C6A3 + +# Verify that the correct key was imported +gpg --list-keys +# You should see the below output +pub ed25519 2022-08-31 [SC] + 88AD93AC4589FD090FF3B8D12768C43E8803C6A3 +uid [ unknown] bitcoindevkit-bindings +sub cv25519 2022-08-31 [E] +``` + +2. Download the binary artifacts and corresponding signature files. +- from [bdk-jvm] + - `bdk-jvm-.jar` + - `bdk-jvm-.jar.asc` +- from [bdk-android] + - `bdk-android-.aar` + - `bdk-android-.aar.asc` + +3. Verify the signatures. +```shell +gpg --verify bdk-jvm-.jar.asc +gpg --verify bdk-android-.aar.asc + +# you should see a "Good signature" result +gpg: Good signature from "bitcoindevkit-bindings " [unknown] +``` + +### PGP Metadata +Full key ID: `88AD 93AC 4589 FD09 0FF3 B8D1 2768 C43E 8803 C6A3` +Fingerprint: `2768C43E8803C6A3` +Name: `bitcoindevkit-bindings` +Email: `bindings@bitcoindevkit.org` + +[Kotlin]: https://kotlinlang.org/ +[Android Studio]: https://developer.android.com/studio/ +[`bdk`]: https://github.com/bitcoindevkit/bdk +[`bdk-ffi`]: https://github.com/bitcoindevkit/bdk-ffi +["Getting Started (Developer)"]: https://github.com/bitcoindevkit/bdk-ffi#getting-started-developer +[Gradle Nexus Publish Plugin]: https://github.com/gradle-nexus/publish-plugin +[bdk-jvm]: https://search.maven.org/artifact/org.bitcoindevkit/bdk-jvm/0.9.0/jar +[bdk-android]: https://search.maven.org/artifact/org.bitcoindevkit/bdk-android/0.9.0/aar diff --git a/bdk-kotlin/api-docs/Module1.md b/bdk-kotlin/api-docs/Module1.md new file mode 100644 index 0000000..d21953b --- /dev/null +++ b/bdk-kotlin/api-docs/Module1.md @@ -0,0 +1,4 @@ +# Module bdk-android +The [bitcoindevkit](https://bitcoindevkit.org/) language bindings library for Android. + +# Package org.bitcoindevkit diff --git a/bdk-kotlin/api-docs/Module2.md b/bdk-kotlin/api-docs/Module2.md new file mode 100644 index 0000000..8a6eef7 --- /dev/null +++ b/bdk-kotlin/api-docs/Module2.md @@ -0,0 +1,4 @@ +# Module bdk-jvm +The [bitcoindevkit](https://bitcoindevkit.org/) language bindings library for Kotlin and Java on the JVM. + +# Package org.bitcoindevkit diff --git a/bdk-kotlin/api-docs/build.gradle.kts b/bdk-kotlin/api-docs/build.gradle.kts new file mode 100644 index 0000000..b969511 --- /dev/null +++ b/bdk-kotlin/api-docs/build.gradle.kts @@ -0,0 +1,46 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.7.10" + + // API docs + id("org.jetbrains.dokka") version "1.7.10" +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} + +// tasks.withType().configureEach { +// dokkaSourceSets { +// named("main") { +// moduleName.set("bdk-android") +// moduleVersion.set("0.11.0") +// includes.from("Module1.md") +// samples.from("src/test/kotlin/org/bitcoindevkit/Samples.kt") +// } +// } +// } + +tasks.withType().configureEach { + dokkaSourceSets { + named("main") { + moduleName.set("bdk-jvm") + moduleVersion.set("0.11.0") + includes.from("Module2.md") + samples.from("src/test/kotlin/org/bitcoindevkit/Samples.kt") + } + } +} diff --git a/bdk-kotlin/api-docs/deploy.sh b/bdk-kotlin/api-docs/deploy.sh new file mode 100644 index 0000000..dd6e442 --- /dev/null +++ b/bdk-kotlin/api-docs/deploy.sh @@ -0,0 +1,8 @@ +./gradlew dokkaHtml +cd build/dokka/html +git init . +git add . +git switch --create gh-pages +git commit -m "Deploy" +git remote add origin git@github.com:bitcoindevkit/bdk-kotlin.git +git push --set-upstream origin gh-pages --force diff --git a/bdk-kotlin/api-docs/gradle.properties b/bdk-kotlin/api-docs/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/bdk-kotlin/api-docs/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.jar b/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.properties b/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..60c76b3 --- /dev/null +++ b/bdk-kotlin/api-docs/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/bdk-kotlin/api-docs/gradlew b/bdk-kotlin/api-docs/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/bdk-kotlin/api-docs/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/bdk-kotlin/api-docs/gradlew.bat b/bdk-kotlin/api-docs/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/bdk-kotlin/api-docs/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bdk-kotlin/api-docs/settings.gradle.kts b/bdk-kotlin/api-docs/settings.gradle.kts new file mode 100644 index 0000000..3b7cdb5 --- /dev/null +++ b/bdk-kotlin/api-docs/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "BDK Android and BDK JVM API Docs" diff --git a/bdk-kotlin/api-docs/src/main/kotlin/org/bitcoindevkit/bdk.kt b/bdk-kotlin/api-docs/src/main/kotlin/org/bitcoindevkit/bdk.kt new file mode 100644 index 0000000..6da4d18 --- /dev/null +++ b/bdk-kotlin/api-docs/src/main/kotlin/org/bitcoindevkit/bdk.kt @@ -0,0 +1,582 @@ +package org.bitcoindevkit + +/** + * The cryptocurrency to act on. + * + * @sample org.bitcoindevkit.networkSample + */ +enum class Network { + /** Bitcoin's mainnet. */ + BITCOIN, + + /** Bitcoin’s testnet. */ + TESTNET, + + /** Bitcoin’s signet. */ + SIGNET, + + /** Bitcoin’s regtest. */ + REGTEST, +} + +/** + * A derived address and the index it was found at. + * + * @property index Child index of this address. + * @property address Address. + * + * @sample org.bitcoindevkit.addressInfoSample + */ +data class AddressInfo ( + var index: UInt, + var address: String +) + +/** + * The address index selection strategy to use to derive an address from the wallet’s external descriptor. + * + * If you’re unsure which one to use, use `AddressIndex.NEW`. + * + * @sample org.bitcoindevkit.addressIndexSample + */ +enum class 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. + */ + LAST_UNUSED, +} + +/** + * Balance differentiated in various categories. + * + * @property immature All coinbase outputs not yet matured. + * @property trustedPending Unconfirmed UTXOs generated by a wallet tx. + * @property untrustedPending Unconfirmed UTXOs received from an external wallet. + * @property confirmed Confirmed and immediately spendable balance. + * @property spendable The sum of trustedPending and confirmed coins. + * @property total The whole balance visible to the wallet. + * + * @sample org.bitcoindevkit.balanceSample + */ +data class Balance ( + var immature: ULong, + var trustedPending: ULong, + var untrustedPending: ULong, + var confirmed: ULong, + var spendable: ULong, + var total: ULong +) + +/** + * Type that can contain any of the database configurations defined by the library. + * + * @sample org.bitcoindevkit.memoryDatabaseConfigSample + * @sample org.bitcoindevkit.sqliteDatabaseConfigSample + */ +sealed class DatabaseConfig { + /** Configuration for an in-memory database. */ + object Memory : DatabaseConfig() + + /** Configuration for a Sled database. */ + data class Sled(val config: SledDbConfiguration) : DatabaseConfig() + + /** Configuration for a SQLite database. */ + data class Sqlite(val config: SqliteDbConfiguration) : DatabaseConfig() +} + +/** + * Configuration type for a SQLite database. + * + * @property path Main directory of the DB. + */ +data class SqliteDbConfiguration( + var path: String, +) + +/** + * Configuration type for a SledDB database. + * + * @property path Main directory of the DB. + * @property treeName Name of the database tree, a separated namespace for the data. + */ +data class SledDbConfiguration( + var path: String, + var treeName: String, +) + +/** + * Configuration for an Electrum blockchain. + * + * @property url 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`. + * @property socks5 URL of the socks5 proxy server or a Tor service. + * @property retry Request retry count. + * @property timeout Request timeout (seconds). + * @property stopGap Stop searching addresses for transactions after finding an unused gap of this length. + * + * @sample org.bitcoindevkit.electrumBlockchainConfigSample + */ +data class ElectrumConfig ( + var url: String, + var socks5: String?, + var retry: UByte, + var timeout: UByte?, + var stopGap: ULong +) + +/** + * Configuration for an Esplora blockchain. + * + * @property baseUrl Base URL of the esplora service, e.g. `https://blockstream.info/api/`. + * @property proxy Optional URL of the proxy to use to make requests to the Esplora server. + * @property concurrency Number of parallel requests sent to the esplora service (default: 4). + * @property stopGap Stop searching addresses for transactions after finding an unused gap of this length. + * @property timeout Socket timeout. + */ +data class EsploraConfig ( + var baseUrl: String, + var proxy: String?, + var concurrency: UByte?, + var stopGap: ULong, + var timeout: ULong? +) + +/** + * Type that can contain any of the blockchain configurations defined by the library. + * + * @sample org.bitcoindevkit.electrumBlockchainConfigSample + */ +sealed class BlockchainConfig { + /** Electrum client. */ + data class Electrum(val config: ElectrumConfig) : BlockchainConfig() + + /** Esplora client. */ + data class Esplora(val config: EsploraConfig) : BlockchainConfig() +} + +/** + * A wallet transaction. + * + * @property fee Fee value (sats) if available. 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. + * @property received Received value (sats) Sum of owned outputs of this transaction. + * @property sent Sent value (sats) Sum of owned inputs of this transaction. + * @property txid Transaction id. + * @property confirmationTime If the transaction is confirmed, [BlockTime] contains height and timestamp of the block containing the transaction. This property is null for unconfirmed transactions. + */ +data class TransactionDetails ( + var fee: ULong?, + var received: ULong, + var sent: ULong, + var txid: String, + var confirmationTime: BlockTime? +) + +/** + * A blockchain backend. + * + * @constructor Create the new blockchain client. + * + * @param config The blockchain configuration required. + * + * @sample org.bitcoindevkit.blockchainSample + */ +class Blockchain( + config: BlockchainConfig +) { + /** Broadcast a transaction. */ + fun broadcast(psbt: PartiallySignedBitcoinTransaction): String {} + + /** Get the current height of the blockchain. */ + fun getHeight(): UInt {} + + /** Get the block hash of a given block. */ + fun getBlockHash(height: UInt): String {} +} + +/** + * A partially signed bitcoin transaction. + * + * @constructor Build a new Partially Signed Bitcoin Transaction. + * + * @param psbtBase64 The PSBT in base64 format. + */ +class PartiallySignedBitcoinTransaction(psbtBase64: String) { + /** Return the PSBT in string format, using a base64 encoding. */ + fun serialize(): String {} + + /** Get the txid of the PSBT. */ + fun txid(): String {} + + /** Return the transaction as bytes. */ + fun `extractTx`(): List + + /** + * Combines this PartiallySignedTransaction with another PSBT as described by BIP 174. + * In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)` + */ + fun combine(other: PartiallySignedBitcoinTransaction): PartiallySignedBitcoinTransaction +} + +/** + * A reference to a transaction output. + * + * @property txid The referenced transaction’s txid. + * @property vout The index of the referenced output in its transaction’s vout. + */ +data class OutPoint ( + var txid: String, + var vout: UInt +) + +/** + * A transaction output, which defines new coins to be created from old ones. + * + * @property value The value of the output, in satoshis. + * @property address The address of the output. + */ +data class TxOut ( + var value: ULong, + var address: String +) + +/** + * An unspent output owned by a [Wallet]. + * + * @property outpoint Reference to a transaction output. + * @property txout Transaction output. + * @property keychain Type of keychain. + * @property isSpent Whether this UTXO is spent or not. + */ +data class LocalUtxo ( + var outpoint: OutPoint, + var txout: TxOut, + var keychain: KeychainKind, + var isSpent: Boolean +) + +/** + * Types of keychains. + */ +enum class KeychainKind { + /** External. */ + EXTERNAL, + + /** Internal, usually used for change outputs. */ + INTERNAL, +} + +/** + * Block height and timestamp of a block. + * + * @property height Confirmation block height. + * @property timestamp Confirmation block timestamp. + */ +data class BlockTime ( + var height: UInt, + var timestamp: ULong, +) + +/** + * 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. + * + * @constructor Create a BDK wallet. + * + * @param descriptor The main (or "external") descriptor. + * @param changeDescriptor The change (or "internal") descriptor. + * @param network The network to act on. + * @param databaseConfig The database configuration. + * + * @sample org.bitcoindevkit.walletSample + */ +class Wallet( + descriptor: String, + changeDescriptor: String, + network: Network, + databaseConfig: DatabaseConfig, +) { + /** + * 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]. + */ + fun getAddress(addressIndex: AddressIndex): AddressInfo {} + + /** 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. */ + fun getBalance(): ULong {} + + /** Sign a transaction with all the wallet’s signers. */ + fun sign(psbt: PartiallySignedBitcoinTransaction): Boolean {} + + /** 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. */ + fun listTransactions(): List {} + + /** Get the Bitcoin network the wallet is using. */ + fun network(): Network {} + + /** Sync the internal database with the blockchain. */ + fun sync(blockchain: Blockchain, progress: Progress?) {} + + /** 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. */ + fun listUnspent(): List {} +} + +/** + * Class that logs at level INFO every update received (if any). + */ +class Progress { + /** 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. */ + fun update(progress: Float, message: String?) {} +} + +/** + * 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. + */ +class TxBuilder() { + /** Add data as an output using OP_RETURN. */ + fun addData(data: List): TxBuilder {} + + /** Add a recipient to the internal list. */ + fun addRecipient(script: Script, amount: ULong): TxBuilder {} + + /** Set the list of recipients by providing a list of [AddressAmount]. */ + fun setRecipients(recipients: List): TxBuilder {} + + /** 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. */ + fun addUnspendable(unspendable: OutPoint): TxBuilder {} + + /** 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. */ + fun addUtxo(outpoint: OutPoint): TxBuilder {} + + /** + * 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. + */ + fun addUtxos(outpoints: List): TxBuilder {} + + /** Do not spend change outputs. This effectively adds all the change outputs to the "unspendable" list. See [TxBuilder.unspendable]. */ + fun doNotSpendChange(): TxBuilder {} + + /** Only spend utxos added by [add_utxo]. The wallet will not add additional utxos to the transaction even if they are needed to make the transaction valid. */ + fun manuallySelectedOnly(): TxBuilder {} + + /** Only spend change outputs. This effectively adds all the non-change outputs to the "unspendable" list. See [TxBuilder.unspendable]. */ + fun onlySpendChange(): TxBuilder {} + + /** + * 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. + */ + fun unspendable(unspendable: List): TxBuilder {} + + /** Set a custom fee rate. */ + fun feeRate(satPerVbyte: Float): TxBuilder {} + + /** Set an absolute fee. */ + fun feeAbsolute(feeAmount: ULong): TxBuilder {} + + /** Spend all the available inputs. This respects filters like [TxBuilder.unspendable] and the change policy. */ + fun drainWallet(): TxBuilder {} + + /** + * 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 [drainTo] without setting any ordinary recipients + * with [addRecipient] (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 [addUtxos], or set + * [drainWallet] to spend all of them. When bumping the fees of a transaction made with this option, + * you probably want to use [BumpFeeTxBuilder.allowShrinking] to allow this output to be reduced to pay for the extra fees. + */ + fun drainTo(address: String): TxBuilder {} + + /** Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. */ + fun enableRbf(): TxBuilder {} + + /** + * 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. + */ + fun enableRbfWithSequence(nsequence: UInt): TxBuilder {} + + /** Finish building the transaction. Returns a [TxBuilderResult]. */ + fun finish(wallet: Wallet): TxBuilderResult {} +} + +/** + * A object holding an ScriptPubKey and an amount. + * + * @property script The ScriptPubKey. + * @property amount The amount. + */ +data class AddressAmount ( + var script: Script, + var amount: ULong +) + +/** + * The BumpFeeTxBuilder is used to bump the fee on a transaction that has been broadcast and has its RBF flag set to true. + */ +class BumpFeeTxBuilder() { + /** + * Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this scriptPubKey + * 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 scriptPubkey can’t be found among the recipients of the transaction we are bumping. + */ + fun allowShrinking(address: String): BumpFeeTxBuilder {} + + /** Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. */ + fun enableRbf(): BumpFeeTxBuilder {} + + /** + * 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. + */ + fun enableRbfWithSequence(nsequence: UInt): BumpFeeTxBuilder {} + + /** Finish building the transaction. Returns a [TxBuilderResult]. */ + fun finish(wallet: Wallet): TxBuilderResult {} +} + +/** + * A BIP-32 derivation path. + * + * @param path The derivation path. Must start with `m`. Use this type to derive or extend a [DescriptorSecretKey] + * or [DescriptorPublicKey]. + */ +class DerivationPath(path: String) {} + +/** + * An extended secret key. + * + * @param network The network this DescriptorSecretKey is to be used on. + * @param mnemonic The mnemonic. + * @param password The optional passphrase that can be provided as per BIP-39. + * + * @sample org.bitcoindevkit.descriptorSecretKeyDeriveSample + * @sample org.bitcoindevkit.descriptorSecretKeyExtendSample + */ +class DescriptorSecretKey(network: Network, mnemonic: Mnemonic, password: String?) { + /** Derive a private descriptor at a given path. */ + fun derive(path: DerivationPath): DescriptorSecretKey {} + + /** Extend the private descriptor with a custom path. */ + fun extend(path: DerivationPath): DescriptorSecretKey {} + + /** Return the public version of the descriptor. */ + fun asPublic(): DescriptorPublicKey {} + + /* Return the raw private key as bytes. */ + fun secretBytes(): List + + /** Return the private descriptor as a string. */ + fun asString(): String {} +} + +/** + * An extended public key. + * + * @param network The network this DescriptorPublicKey is to be used on. + * @param mnemonic The mnemonic. + * @param password The optional passphrase that can be provided as per BIP-39. + */ +class DescriptorPublicKey(network: Network, mnemonic: String, password: String?) { + /** Derive a public descriptor at a given path. */ + fun derive(path: DerivationPath): DescriptorSecretKey + + /** Extend the public descriptor with a custom path. */ + fun extend(path: DerivationPath): DescriptorSecretKey + + /** Return the public descriptor as a string. */ + fun asString(): String +} + +/** + * An enum describing entropy length (aka word count) in the mnemonic. + */ +enum class WordCount { + /** 12 words mnemonic (128 bits entropy). */ + WORDS12, + + /** 15 words mnemonic (160 bits entropy). */ + WORDS15, + + /** 18 words mnemonic (192 bits entropy). */ + WORDS18, + + /** 21 words mnemonic (224 bits entropy). */ + WORDS21, + + /** 24 words mnemonic (256 bits entropy). */ + WORDS24, +} + +/** + * The value returned from calling the `.finish()` method on the [TxBuilder] or [BumpFeeTxBuilder]. + * + * @property psbt The PSBT + * @property transactionDetails The transaction details. + * + * @sample org.bitcoindevkit.txBuilderResultSample1 + * @sample org.bitcoindevkit.txBuilderResultSample2 + */ +data class TxBuilderResult ( + var psbt: PartiallySignedBitcoinTransaction, + var transactionDetails: TransactionDetails +) + +/** + * A bitcoin script. + */ +class Script(rawOutputScript: List) + +/** + * A bitcoin address. + * + * @param address The address in string format. + */ +class Address(address: String) { + /* Return the ScriptPubKey. */ + fun scriptPubkey(): Script +} + +/** + * Mnemonic phrases are a human-readable version of the private keys. Supported number of words are 12, 15, 18, 21 and 24. + * + * @constructor Generates Mnemonic with a random entropy. + * @param mnemonic The mnemonic as a string of space-separated words. + * + * @sample org.bitcoindevkit.mnemonicSample + */ +class Mnemonic(mnemonic: String) { + /* Returns Mnemonic as string */ + fun asString(): String + + /* Parse a Mnemonic from a given string. */ + fun fromString(): Mnemonic + + /* + * 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. + */ + fun fromEntropy(): Mnemonic +} diff --git a/bdk-kotlin/api-docs/src/test/kotlin/org/bitcoindevkit/Samples.kt b/bdk-kotlin/api-docs/src/test/kotlin/org/bitcoindevkit/Samples.kt new file mode 100644 index 0000000..34840ec --- /dev/null +++ b/bdk-kotlin/api-docs/src/test/kotlin/org/bitcoindevkit/Samples.kt @@ -0,0 +1,233 @@ +package org.bitcoindevkit + +fun networkSample() { + val wallet = Wallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + network = Network.TESTNET, + databaseConfig = DatabaseConfig.Memory + ) +} + +fun balanceSample() { + object LogProgress : Progress { + override fun update(progress: Float, message: String?) {} + } + + val memoryDatabaseConfig = DatabaseConfig.Memory + private val blockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + null, + 5u, + null, + 200u + ) + ) + val wallet = Wallet(descriptor, null, Network.TESTNET, memoryDatabaseConfig) + val blockchain = Blockchain(blockchainConfig) + wallet.sync(blockchain, LogProgress) + + val balance: Balance = wallet.getBalance() + println("Total wallet balance is ${balance.total}") +} + +fun electrumBlockchainConfigSample() { + val blockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + url = "ssl://electrum.blockstream.info:60002", + socks5 = null, + retry = 5u, + timeout = null, + stopGap = 200u + ) + ) +} + +fun memoryDatabaseConfigSample() { + val memoryDatabaseConfig = DatabaseConfig.Memory +} + +fun sqliteDatabaseConfigSample() { + val databaseConfig = DatabaseConfig.Sqlite(SqliteDbConfiguration("bdk-sqlite")) +} + +fun addressIndexSample() { + val wallet: Wallet = Wallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + network = Network.TESTNET, + databaseConfig = DatabaseConfig.Memory + ) + + fun getLastUnusedAddress(): AddressInfo { + return wallet.getAddress(AddressIndex.LAST_UNUSED) + } +} + +fun addressInfoSample() { + val wallet: Wallet = Wallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + network = Network.TESTNET, + databaseConfig = DatabaseConfig.Memory + ) + + fun getLastUnusedAddress(): AddressInfo { + return wallet.getAddress(AddressIndex.NEW) + } + + val newAddress: AddressInfo = getLastUnusedAddress() + + println("New address at index ${newAddress.index} is ${newAddress.address}") +} + +fun blockchainSample() { + val blockchainConfig: BlockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + electrumURL, + null, + 5u, + null, + 10u + ) + ) + + val blockchain: Blockchain = Blockchain(blockchainConfig) + + blockchain.broadcast(signedPsbt) +} + + +fun txBuilderResultSample1() { + val faucetAddress = Address("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") + // TxBuilderResult is a data class, which means you can use destructuring declarations on it to + // open it up in its component parts + val (psbt, txDetails) = TxBuilder() + .addRecipient(faucetAddress.scriptPubkey(), 1000u) + .feeRate(1.2f) + .finish(wallet) + + println("Txid is ${txDetails.txid}") + wallet.sign(psbt) +} + +fun txBuilderResultSample2() { + val faucetAddress = Address("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") + val txBuilderResult: TxBuilderResult = TxBuilder() + .addRecipient(faucetAddress.scriptPubkey(), 1000u) + .feeRate(1.2f) + .finish(wallet) + + val psbt = txBuilderResult.psbt + val txDetails = txBuilderResult.transactionDetails + + println("Txid is ${txDetails.txid}") + wallet.sign(psbt) +} + +fun descriptorSecretKeyExtendSample() { + // The `DescriptorSecretKey.extend()` method allows you to extend a key to any given path. + + // val mnemonic: String = generateMnemonic(WordCount.WORDS12) + val mnemonic: Mnemonic = Mnemonic("scene change clap smart together mind wheel knee clip normal trial unusual") + + // the initial DescriptorSecretKey will always be at the "master" node, + // i.e. the derivation path is empty + val bip32RootKey: DescriptorSecretKey = DescriptorSecretKey( + network = Network.TESTNET, + mnemonic = mnemonic, + password = "" + ) + println(bip32RootKey.asString()) + // tprv8ZgxMBicQKsPfM8Trx2apvdEkmxbJkYY3ZsmcgKb2bfnLNcBhtCstqQTeFesMRLEJXpjGDinAUJUHprXMwph8dQBdS1HAoxEis8Knimxovf/* + + // the derive method will also automatically apply the wildcard (*) to your path, + // i.e the following will generate the typical testnet BIP84 external wallet path + // m/84h/1h/0h/0/* + val bip84ExternalPath: DerivationPath = DerivationPath("m/84h/1h/0h/0") + val externalExtendedKey: DescriptorSecretKey = bip32RootKey.extend(bip84ExternalPath).asString() + println(externalExtendedKey) + // tprv8ZgxMBicQKsPfM8Trx2apvdEkmxbJkYY3ZsmcgKb2bfnLNcBhtCstqQTeFesMRLEJXpjGDinAUJUHprXMwph8dQBdS1HAoxEis8Knimxovf/84'/1'/0'/0/* + + // to create the descriptor you'll need to use this extended key in a descriptor function, + // i.e. wpkh(), tr(), etc. + val externalDescriptor = "wpkh($externalExtendedKey)" +} + +fun descriptorSecretKeyDeriveSample() { + // The DescriptorSecretKey.derive() method allows you to derive an extended key for a given + // node in the derivation tree (for example to create an xpub for a particular account) + + val mnemonic: Mnemonic = Mnemonic("scene change clap smart together mind wheel knee clip normal trial unusual") + val bip32RootKey: DescriptorSecretKey = DescriptorSecretKey( + network = Network.TESTNET, + mnemonic = mnemonic, + password = "" + ) + + val bip84Account0: DerivationPath = DerivationPath("m/84h/1h/0h") + val xpubAccount0: DescriptorSecretKey = bip32RootKey.derive(bip84Account0) + println(xpubAccount0.asString()) + // [5512949b/84'/1'/0']tprv8ghw3FWfWTeLCEXcr8f8Q8Lz4QPCELYv3jhBXjAm7XagA6R5hreeWLTJeLBfMj7Ni6Q3PdV1o8NbvNBHE59W97EkRJSU4JkvTQjaNUmQubE/* + + val internalPath: DerivationPath = DerivationPath("m/0") + val externalExtendedKey = xpubAccount0.extend(internalPath).asString() + println(externalExtendedKey) + // [5512949b/84'/1'/0']tprv8ghw3FWfWTeLCEXcr8f8Q8Lz4QPCELYv3jhBXjAm7XagA6R5hreeWLTJeLBfMj7Ni6Q3PdV1o8NbvNBHE59W97EkRJSU4JkvTQjaNUmQubE/0/* + + // to create the descriptor you'll need to use this extended key in a descriptor function, + // i.e. wpkh(), tr(), etc. + val externalDescriptor = "wpkh($externalExtendedKey)" +} + +fun createTransaction() { + val wallet = BdkWallet( + descriptor = externalDescriptor, + changeDescriptor = internalDescriptor, + network = Network.TESTNET, + databaseConfig = memoryDatabaseConfig, + ) + val blockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + null, + 5u, + null, + 200u + ) + ) + + val paymentAddress: Address = Address("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") + val (psbt, txDetails) = TxBuilder() + .addRecipient(faucetAddress.scriptPubkey(), 1000u) + .feeRate(1.2f) + .finish(wallet) + + wallet.sign(psbt) + blockchain.broadcast(psbt) +} + +fun walletSample() { + val externalDescriptor = "wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEfVULesmhEfZYyBXdE/84h/1h/0h/0/*)" + val internalDescriptor = "wpkh(tprv8hwWMmPE4BVNxGdVt3HhEERZhondQvodUY7Ajyseyhudr4WabJqWKWLr4Wi2r26CDaNCQhhxEfVULesmhEfZYyBXdE/84h/1h/0h/1/*)" + val sqliteDatabaseConfig = DatabaseConfig.Sqlite(SqliteDbConfiguration("bdk-sqlite")) + + val wallet = BdkWallet( + descriptor = externalDescriptor, + changeDescriptor = internalDescriptor, + network = Network.TESTNET, + databaseConfig = sqliteDatabaseConfig, + ) +} + +fun mnemonicSample() { + val mnemonic0: Mnemonic = Mnemonic(WordCount.WORDS12) + + val mnemonic1: Mnemonic = Mnemonic.fromString("scene change clap smart together mind wheel knee clip normal trial unusual") + + val entropy: List = listOf(0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u) + val mnemonic2: Mnemonic = Mnemonic.fromEntropy(entropy) + + println(mnemonic0.asString(), mnemonic1.asString(), mnemonic2.asString()) +} diff --git a/bdk-kotlin/bdk-android/build.gradle.kts b/bdk-kotlin/bdk-android/build.gradle.kts new file mode 100644 index 0000000..8304c9c --- /dev/null +++ b/bdk-kotlin/bdk-android/build.gradle.kts @@ -0,0 +1,33 @@ +buildscript { + repositories { + google() + } + dependencies { + classpath("com.android.tools.build:gradle:7.1.2") + } +} + +plugins { + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" +} + +// These properties are required here so that the nexus publish-plugin +// finds a staging profile with the correct group (group is otherwise set as "") +// and knows whether to publish to a SNAPSHOT repository or not +// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin +group = "org.bitcoindevkit" +version = "0.12.0-SNAPSHOT" + +nexusPublishing { + repositories { + create("sonatype") { + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + + val ossrhUsername: String? by project + val ossrhPassword: String? by project + username.set(ossrhUsername) + password.set(ossrhPassword) + } + } +} diff --git a/bdk-kotlin/bdk-android/gradle.properties b/bdk-kotlin/bdk-android/gradle.properties new file mode 100644 index 0000000..58f55b9 --- /dev/null +++ b/bdk-kotlin/bdk-android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=true +android.enableJetifier=true +kotlin.code.style=official diff --git a/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.jar b/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.properties b/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/bdk-kotlin/bdk-android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/bdk-kotlin/bdk-android/gradlew b/bdk-kotlin/bdk-android/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/bdk-kotlin/bdk-android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/bdk-kotlin/bdk-android/gradlew.bat b/bdk-kotlin/bdk-android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/bdk-kotlin/bdk-android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bdk-kotlin/bdk-android/lib/build.gradle.kts b/bdk-kotlin/bdk-android/lib/build.gradle.kts new file mode 100644 index 0000000..6164f29 --- /dev/null +++ b/bdk-kotlin/bdk-android/lib/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") version "1.6.10" + id("maven-publish") + id("signing") + + // Custom plugin to generate the native libs and bindings file + id("org.bitcoindevkit.plugins.generate-android-bindings") +} + +repositories { + mavenCentral() + google() +} + +android { + compileSdk = 31 + + defaultConfig { + minSdk = 21 + targetSdk = 31 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(file("proguard-android-optimize.txt"), file("proguard-rules.pro")) + } + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +dependencies { + implementation("net.java.dev.jna:jna:5.8.0@aar") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") + implementation("androidx.appcompat:appcompat:1.4.0") + implementation("androidx.core:core-ktx:1.7.0") + api("org.slf4j:slf4j-api:1.7.30") + + androidTestImplementation("com.github.tony19:logback-android:2.0.0") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") +} + +afterEvaluate { + publishing { + publications { + create("maven") { + groupId = "org.bitcoindevkit" + artifactId = "bdk-android" + version = "0.12.0-SNAPSHOT" + + from(components["release"]) + pom { + name.set("bdk-android") + description.set("Bitcoin Dev Kit Kotlin language bindings.") + url.set("https://bitcoindevkit.org") + licenses { + license { + name.set("APACHE 2.0") + url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-APACHE") + } + license { + name.set("MIT") + url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") + } + } + developers { + developer { + id.set("notmandatory") + name.set("Steve Myers") + email.set("notmandatory@noreply.github.org") + } + developer { + id.set("artfuldev") + name.set("Sudarsan Balaji") + email.set("sudarsan.balaji@artfuldev.com") + } + } + scm { + connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") + developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") + url.set("https://github.com/bitcoindevkit/bdk-ffi/tree/master") + } + } + } + } + } +} + +signing { + val signingKeyId: String? by project + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications) +} diff --git a/bdk-kotlin/bdk-android/lib/proguard-rules.pro b/bdk-kotlin/bdk-android/lib/proguard-rules.pro new file mode 100644 index 0000000..264284c --- /dev/null +++ b/bdk-kotlin/bdk-android/lib/proguard-rules.pro @@ -0,0 +1,28 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# for JNA +-dontwarn java.awt.* +-keep class com.sun.jna.* { *; } +-keep class org.bitcoindevkit.* { *; } +-keepclassmembers class * extends org.bitcoindevkit.* { public *; } +-keepclassmembers class * extends com.sun.jna.* { public *; } diff --git a/bdk-kotlin/bdk-android/lib/src/androidTest/assets/logback.xml b/bdk-kotlin/bdk-android/lib/src/androidTest/assets/logback.xml new file mode 100644 index 0000000..efb1f6b --- /dev/null +++ b/bdk-kotlin/bdk-android/lib/src/androidTest/assets/logback.xml @@ -0,0 +1,14 @@ + + + + %logger{12} + + + [%-20thread] %msg + + + + + + + diff --git a/bdk-kotlin/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt b/bdk-kotlin/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt new file mode 100644 index 0000000..1fb2132 --- /dev/null +++ b/bdk-kotlin/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt @@ -0,0 +1,81 @@ +package org.bitcoindevkit + +import org.junit.Assert.* +import org.junit.Test +import android.app.Application +import android.content.Context.MODE_PRIVATE +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class AndroidLibTest { + + private fun getTestDataDir(): String { + val context = ApplicationProvider.getApplicationContext() + return context.getDir("bdk-test", MODE_PRIVATE).toString() + } + + private fun cleanupTestDataDir(testDataDir: String) { + File(testDataDir).deleteRecursively() + } + + class LogProgress : Progress { + private val log: Logger = LoggerFactory.getLogger(AndroidLibTest::class.java) + + override fun update(progress: Float, message: String?) { + log.debug("Syncing...") + } + } + + private val descriptor = + "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)" + + private val databaseConfig = DatabaseConfig.Memory + + private val blockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + null, + 5u, + null, + 100u + ) + ) + + @Test + fun memoryWalletNewAddress() { + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val address = wallet.getAddress(AddressIndex.NEW).address + assertEquals("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address) + } + + @Test + fun memoryWalletSyncGetBalance() { + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val blockchain = Blockchain(blockchainConfig) + wallet.sync(blockchain, LogProgress()) + val balance: Balance = wallet.getBalance() + assertTrue(balance.total > 0u) + } + + @Test + fun sqliteWalletSyncGetBalance() { + val testDataDir = getTestDataDir() + "/bdk-wallet.sqlite" + val databaseConfig = DatabaseConfig.Sqlite(SqliteDbConfiguration(testDataDir)) + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val blockchain = Blockchain(blockchainConfig) + wallet.sync(blockchain, LogProgress()) + val balance: Balance = wallet.getBalance() + assertTrue(balance.total > 0u) + cleanupTestDataDir(testDataDir) + } +} diff --git a/bdk-kotlin/bdk-android/lib/src/main/AndroidManifest.xml b/bdk-kotlin/bdk-android/lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6aa0e82 --- /dev/null +++ b/bdk-kotlin/bdk-android/lib/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/bdk-kotlin/bdk-android/plugins/README.md b/bdk-kotlin/bdk-android/plugins/README.md new file mode 100644 index 0000000..40025f9 --- /dev/null +++ b/bdk-kotlin/bdk-android/plugins/README.md @@ -0,0 +1,17 @@ +# Readme +The purpose of this directory is to host the Gradle plugin that adds tasks for building the native binaries required by `bdk-android`, and building the language bindings files. + +The plugin is applied to the `build.gradle.kts` file in `bdk-android` through the `plugins` block: +```kotlin +// bdk-android +plugins { + id("org.bitcoindevkit.plugins.generate-android-bindings") +} +``` + +It adds a series of tasks which are brought together into an aggregate task called `buildAndroidLib`. + +This aggregate task: +1. Builds the native libraries using `bdk-ffi` +2. Places them in the correct resource directories +3. Builds the bindings file diff --git a/bdk-kotlin/bdk-android/plugins/build.gradle.kts b/bdk-kotlin/bdk-android/plugins/build.gradle.kts new file mode 100644 index 0000000..37737bc --- /dev/null +++ b/bdk-kotlin/bdk-android/plugins/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-gradle-plugin") + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("uniFfiAndroidBindings") { + id = "org.bitcoindevkit.plugins.generate-android-bindings" + implementationClass = "org.bitcoindevkit.plugins.UniFfiAndroidPlugin" + } + } +} diff --git a/bdk-kotlin/bdk-android/plugins/settings.gradle.kts b/bdk-kotlin/bdk-android/plugins/settings.gradle.kts new file mode 100644 index 0000000..ca62a7a --- /dev/null +++ b/bdk-kotlin/bdk-android/plugins/settings.gradle.kts @@ -0,0 +1,8 @@ +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + } +} + +// include(":plugins") diff --git a/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt b/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt new file mode 100644 index 0000000..8f586b6 --- /dev/null +++ b/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt @@ -0,0 +1,14 @@ +package org.bitcoindevkit.plugins + + +val operatingSystem: OS = when { + System.getProperty("os.name").contains("mac", ignoreCase = true) -> OS.MAC + System.getProperty("os.name").contains("linux", ignoreCase = true) -> OS.LINUX + else -> OS.OTHER +} + +enum class OS { + MAC, + LINUX, + OTHER, +} diff --git a/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiAndroidPlugin.kt b/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiAndroidPlugin.kt new file mode 100644 index 0000000..55b14bc --- /dev/null +++ b/bdk-kotlin/bdk-android/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiAndroidPlugin.kt @@ -0,0 +1,180 @@ +package org.bitcoindevkit.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.Exec +import org.gradle.kotlin.dsl.environment +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.register + +internal class UniFfiAndroidPlugin : Plugin { + override fun apply(target: Project): Unit = target.run { + val llvmArchPath = when (operatingSystem) { + OS.MAC -> "darwin-x86_64" + OS.LINUX -> "linux-x86_64" + OS.OTHER -> throw Error("Cannot build Android library from current architecture") + } + + // arm64-v8a is the most popular hardware architecture for Android + val buildAndroidAarch64Binary by tasks.register("buildAndroidAarch64Binary") { + + workingDir("${projectDir}/../../bdk-ffi") + val cargoArgs: MutableList = + mutableListOf("build", "--profile", "release-smaller", "--target", "aarch64-linux-android") + + executable("cargo") + args(cargoArgs) + + // if ANDROID_NDK_ROOT is not set then set it to github actions default + if (System.getenv("ANDROID_NDK_ROOT") == null) { + environment( + Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle") + ) + } + + environment( + // add build toolchain to PATH + Pair("PATH", + "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), + + Pair("CFLAGS", "-D__ANDROID_API__=21"), + Pair("CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER", "aarch64-linux-android21-clang"), + Pair("CC", "aarch64-linux-android21-clang") + ) + + doLast { + println("Native library for bdk-android on aarch64 built successfully") + } + } + + // the x86_64 version of the library is mostly used by emulators + val buildAndroidX86_64Binary by tasks.register("buildAndroidX86_64Binary") { + + workingDir("${project.projectDir}/../../bdk-ffi") + val cargoArgs: MutableList = + mutableListOf("build", "--profile", "release-smaller", "--target", "x86_64-linux-android") + + executable("cargo") + args(cargoArgs) + + // if ANDROID_NDK_ROOT is not set then set it to github actions default + if (System.getenv("ANDROID_NDK_ROOT") == null) { + environment( + Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle") + ) + } + + environment( + // add build toolchain to PATH + Pair("PATH", + "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), + + Pair("CFLAGS", "-D__ANDROID_API__=21"), + Pair("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER", "x86_64-linux-android21-clang"), + Pair("CC", "x86_64-linux-android21-clang") + ) + + doLast { + println("Native library for bdk-android on x86_64 built successfully") + } + } + + // armeabi-v7a version of the library for older 32-bit Android hardware + val buildAndroidArmv7Binary by tasks.register("buildAndroidArmv7Binary") { + + workingDir("${project.projectDir}/../../bdk-ffi") + val cargoArgs: MutableList = + mutableListOf("build", "--profile", "release-smaller", "--target", "armv7-linux-androideabi") + + executable("cargo") + args(cargoArgs) + + // if ANDROID_NDK_ROOT is not set then set it to github actions default + if (System.getenv("ANDROID_NDK_ROOT") == null) { + environment( + Pair("ANDROID_NDK_ROOT", "${System.getenv("ANDROID_SDK_ROOT")}/ndk-bundle") + ) + } + + environment( + // add build toolchain to PATH + Pair("PATH", + "${System.getenv("PATH")}:${System.getenv("ANDROID_NDK_ROOT")}/toolchains/llvm/prebuilt/$llvmArchPath/bin"), + + Pair("CFLAGS", "-D__ANDROID_API__=21"), + Pair("CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER", + "armv7a-linux-androideabi21-clang"), + Pair("CC", "armv7a-linux-androideabi21-clang") + ) + + doLast { + println("Native library for bdk-android on armv7 built successfully") + } + } + + // move the native libs build by cargo from bdk-ffi/target//release/ + // to their place in the bdk-android library + // the task only copies the available binaries built using the buildAndroidBinary tasks + val moveNativeAndroidLibs by tasks.register("moveNativeAndroidLibs") { + + dependsOn(buildAndroidAarch64Binary) + + into("${project.projectDir}/../lib/src/main/jniLibs/") + + into("arm64-v8a") { + from("${project.projectDir}/../../bdk-ffi/target/aarch64-linux-android/release-smaller/libbdkffi.so") + } + + into("x86_64") { + from("${project.projectDir}/../../bdk-ffi/target/x86_64-linux-android/release-smaller/libbdkffi.so") + } + + into("armeabi-v7a") { + from("${project.projectDir}/../../bdk-ffi/target/armv7-linux-androideabi/release-smaller/libbdkffi.so") + } + + doLast { + println("Native binaries for Android moved to ./lib/src/main/jniLibs/") + } + } + + // generate the bindings using the bdk-ffi-bindgen tool located in the bdk-ffi submodule + val generateAndroidBindings by tasks.register("generateAndroidBindings") { + dependsOn(moveNativeAndroidLibs) + + workingDir("${project.projectDir}/../../bdk-ffi") + executable("cargo") + args( + "run", + "--package", + "bdk-ffi-bindgen", + "--", + "--language", + "kotlin", + "--out-dir", + "../bdk-android/lib/src/main/kotlin" + ) + + doLast { + println("Android bindings file successfully created") + } + } + + // create an aggregate task which will run the required tasks to build the Android libs in order + // the task will also appear in the printout of the ./gradlew tasks task with group and description + tasks.register("buildAndroidLib") { + group = "Bitcoindevkit" + description = "Aggregate task to build Android library" + + dependsOn( + buildAndroidAarch64Binary, + buildAndroidX86_64Binary, + buildAndroidArmv7Binary, + moveNativeAndroidLibs, + generateAndroidBindings + ) + } + } +} diff --git a/bdk-kotlin/bdk-android/settings.gradle.kts b/bdk-kotlin/bdk-android/settings.gradle.kts new file mode 100644 index 0000000..ef7d1eb --- /dev/null +++ b/bdk-kotlin/bdk-android/settings.gradle.kts @@ -0,0 +1,4 @@ +rootProject.name = "bdk-android" + +include(":lib") +includeBuild("plugins") diff --git a/bdk-kotlin/bdk-jvm/build.gradle.kts b/bdk-kotlin/bdk-jvm/build.gradle.kts new file mode 100644 index 0000000..c9807ba --- /dev/null +++ b/bdk-kotlin/bdk-jvm/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" +} + +// These properties are required here so that the nexus publish-plugin +// finds a staging profile with the correct group (group is otherwise set as "") +// and knows whether to publish to a SNAPSHOT repository or not +// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin +group = "org.bitcoindevkit" +version = "0.12.0-SNAPSHOT" + +nexusPublishing { + repositories { + create("sonatype") { + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + + val ossrhUsername: String? by project + val ossrhPassword: String? by project + username.set(ossrhUsername) + password.set(ossrhPassword) + } + } +} diff --git a/bdk-kotlin/bdk-jvm/gradle.properties b/bdk-kotlin/bdk-jvm/gradle.properties new file mode 100644 index 0000000..e12e896 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536m +android.enableJetifier=true +kotlin.code.style=official diff --git a/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.jar b/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.properties b/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/bdk-kotlin/bdk-jvm/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/bdk-kotlin/bdk-jvm/gradlew b/bdk-kotlin/bdk-jvm/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/bdk-kotlin/bdk-jvm/gradlew.bat b/bdk-kotlin/bdk-jvm/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bdk-kotlin/bdk-jvm/lib/build.gradle.kts b/bdk-kotlin/bdk-jvm/lib/build.gradle.kts new file mode 100644 index 0000000..b8ab170 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/lib/build.gradle.kts @@ -0,0 +1,100 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat.* +import org.gradle.api.tasks.testing.logging.TestLogEvent.* + +plugins { + id("org.jetbrains.kotlin.jvm") version "1.6.10" + id("java-library") + id("maven-publish") + id("signing") + + // Custom plugin to generate the native libs and bindings file + id("org.bitcoindevkit.plugins.generate-jvm-bindings") +} + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withSourcesJar() + withJavadocJar() +} + +tasks.withType { + useJUnitPlatform() + + testLogging { + events(PASSED, SKIPPED, FAILED, STANDARD_OUT, STANDARD_ERROR) + exceptionFormat = FULL + showExceptions = true + showCauses = true + showStackTraces = true + } +} + +dependencies { + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") + implementation("net.java.dev.jna:jna:5.8.0") + api("org.slf4j:slf4j-api:1.7.30") + testImplementation("junit:junit:4.13.2") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2") + testImplementation("ch.qos.logback:logback-classic:1.2.3") + testImplementation("ch.qos.logback:logback-core:1.2.3") +} + +afterEvaluate { + publishing { + publications { + create("maven") { + groupId = "org.bitcoindevkit" + artifactId = "bdk-jvm" + version = "0.12.0-SNAPSHOT" + + from(components["java"]) + pom { + name.set("bdk-jvm") + description.set("Bitcoin Dev Kit Kotlin language bindings.") + url.set("https://bitcoindevkit.org") + licenses { + license { + name.set("APACHE 2.0") + url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-APACHE") + } + license { + name.set("MIT") + url.set("https://github.com/bitcoindevkit/bdk/blob/master/LICENSE-MIT") + } + } + developers { + developer { + id.set("notmandatory") + name.set("Steve Myers") + email.set("notmandatory@noreply.github.org") + } + developer { + id.set("artfuldev") + name.set("Sudarsan Balaji") + email.set("sudarsan.balaji@artfuldev.com") + } + } + scm { + connection.set("scm:git:github.com/bitcoindevkit/bdk-ffi.git") + developerConnection.set("scm:git:ssh://github.com/bitcoindevkit/bdk-ffi.git") + url.set("https://github.com/bitcoindevkit/bdk-ffi/tree/master") + } + } + } + } + } +} + +signing { + val signingKeyId: String? by project + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications) +} diff --git a/bdk-kotlin/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/JvmLibTest.kt b/bdk-kotlin/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/JvmLibTest.kt new file mode 100644 index 0000000..a66f8cd --- /dev/null +++ b/bdk-kotlin/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/JvmLibTest.kt @@ -0,0 +1,73 @@ +package org.bitcoindevkit + +import org.junit.Assert.* +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Files + +/** + * Library test, which will execute on linux host. + */ +class JvmLibTest { + + private fun getTestDataDir(): String { + return Files.createTempDirectory("bdk-test").toString() + } + + private fun cleanupTestDataDir(testDataDir: String) { + File(testDataDir).deleteRecursively() + } + + class LogProgress : Progress { + private val log: Logger = LoggerFactory.getLogger(JvmLibTest::class.java) + + override fun update(progress: Float, message: String?) { + log.debug("Syncing...") + } + } + + private val descriptor = + "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)" + + private val databaseConfig = DatabaseConfig.Memory + + private val blockchainConfig = BlockchainConfig.Electrum( + ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + null, + 5u, + null, + 100u + ) + ) + + @Test + fun memoryWalletNewAddress() { + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val address = wallet.getAddress(AddressIndex.NEW).address + assertEquals("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address) + } + + @Test + fun memoryWalletSyncGetBalance() { + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val blockchain = Blockchain(blockchainConfig) + wallet.sync(blockchain, LogProgress()) + val balance: Balance = wallet.getBalance() + assertTrue(balance.total > 0u) + } + + @Test + fun sqliteWalletSyncGetBalance() { + val testDataDir = getTestDataDir() + "/bdk-wallet.sqlite" + val databaseConfig = DatabaseConfig.Sqlite(SqliteDbConfiguration(testDataDir)) + val wallet = Wallet(descriptor, null, Network.TESTNET, databaseConfig) + val blockchain = Blockchain(blockchainConfig) + wallet.sync(blockchain, LogProgress()) + val balance: Balance = wallet.getBalance() + assertTrue(balance.total > 0u) + cleanupTestDataDir(testDataDir) + } +} diff --git a/bdk-kotlin/bdk-jvm/plugins/README.md b/bdk-kotlin/bdk-jvm/plugins/README.md new file mode 100644 index 0000000..b80e5d2 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/plugins/README.md @@ -0,0 +1,16 @@ +# Readme +The purpose of this directory is to host the Gradle plugin that adds tasks for building the native binaries required by bdk-jvm, and building the language bindings files. + +The plugin is applied to the `build.gradle.kts` file through the `plugins` block: +```kotlin +plugins { + id("org.bitcoindevkit.plugin.generate-jvm-bindings") +} +``` + +The plugin adds a series of tasks which are brought together into an aggregate task called `buildJvmLib` for `bdk-jvm`. + +This aggregate task: +1. Builds the native library(ies) using `bdk-ffi` +2. Places it in the correct resource directory +3. Builds the bindings file diff --git a/bdk-kotlin/bdk-jvm/plugins/build.gradle.kts b/bdk-kotlin/bdk-jvm/plugins/build.gradle.kts new file mode 100644 index 0000000..91dfb28 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/plugins/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-gradle-plugin") + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("uniFfiJvmBindings") { + id = "org.bitcoindevkit.plugins.generate-jvm-bindings" + implementationClass = "org.bitcoindevkit.plugins.UniFfiJvmPlugin" + } + } +} diff --git a/bdk-kotlin/bdk-jvm/plugins/settings.gradle.kts b/bdk-kotlin/bdk-jvm/plugins/settings.gradle.kts new file mode 100644 index 0000000..ca62a7a --- /dev/null +++ b/bdk-kotlin/bdk-jvm/plugins/settings.gradle.kts @@ -0,0 +1,8 @@ +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + } +} + +// include(":plugins") diff --git a/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt b/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt new file mode 100644 index 0000000..8f586b6 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/Enums.kt @@ -0,0 +1,14 @@ +package org.bitcoindevkit.plugins + + +val operatingSystem: OS = when { + System.getProperty("os.name").contains("mac", ignoreCase = true) -> OS.MAC + System.getProperty("os.name").contains("linux", ignoreCase = true) -> OS.LINUX + else -> OS.OTHER +} + +enum class OS { + MAC, + LINUX, + OTHER, +} diff --git a/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiJvmPlugin.kt b/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiJvmPlugin.kt new file mode 100644 index 0000000..04e5b13 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/plugins/src/main/kotlin/org/bitcoindevkit/plugins/UniFfiJvmPlugin.kt @@ -0,0 +1,123 @@ +package org.bitcoindevkit.plugins + +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.Exec +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.register + +internal class UniFfiJvmPlugin : Plugin { + override fun apply(target: Project): Unit = target.run { + + // register a task called buildJvmBinaries which will run something like + // cargo build --release --target aarch64-apple-darwin + val buildJvmBinaries by tasks.register("buildJvmBinaries") { + if (operatingSystem == OS.MAC) { + exec { + workingDir("${project.projectDir}/../../bdk-ffi") + executable("cargo") + val cargoArgs: List = listOf("build", "--profile", "release-smaller", "--target", "x86_64-apple-darwin") + args(cargoArgs) + } + exec { + workingDir("${project.projectDir}/../../bdk-ffi") + executable("cargo") + val cargoArgs: List = listOf("build", "--profile", "release-smaller", "--target", "aarch64-apple-darwin") + args(cargoArgs) + } + } else if(operatingSystem == OS.LINUX) { + exec { + workingDir("${project.projectDir}/../../bdk-ffi") + executable("cargo") + val cargoArgs: List = listOf("build", "--profile", "release-smaller", "--target", "x86_64-unknown-linux-gnu") + args(cargoArgs) + } + } + } + + // move the native libs build by cargo from bdk-ffi/target/.../release/ + // to their place in the bdk-jvm library + val moveNativeJvmLibs by tasks.register("moveNativeJvmLibs") { + + // dependsOn(buildJvmBinaryX86_64MacOS, buildJvmBinaryAarch64MacOS, buildJvmBinaryLinux) + dependsOn(buildJvmBinaries) + + data class CopyMetadata(val targetDir: String, val resDir: String, val ext: String) + val libsToCopy: MutableList = mutableListOf() + + if (operatingSystem == OS.MAC) { + libsToCopy.add( + CopyMetadata( + targetDir = "aarch64-apple-darwin", + resDir = "darwin-aarch64", + ext = "dylib" + ) + ) + libsToCopy.add( + CopyMetadata( + targetDir = "x86_64-apple-darwin", + resDir = "darwin-x86-64", + ext = "dylib" + ) + ) + } else if (operatingSystem == OS.LINUX) { + libsToCopy.add( + CopyMetadata( + targetDir = "x86_64-unknown-linux-gnu", + resDir = "linux-x86-64", + ext = "so" + ) + ) + } + + libsToCopy.forEach { + doFirst { + copy { + with(it) { + from("${project.projectDir}/../../bdk-ffi/target/${this.targetDir}/release-smaller/libbdkffi.${this.ext}") + into("${project.projectDir}/../../bdk-jvm/lib/src/main/resources/${this.resDir}/") + } + } + } + } + } + + // generate the bindings using the bdk-ffi-bindgen tool created in the bdk-ffi submodule + val generateJvmBindings by tasks.register("generateJvmBindings") { + + dependsOn(moveNativeJvmLibs) + + workingDir("${project.projectDir}/../../bdk-ffi") + executable("cargo") + args( + "run", + "--package", + "bdk-ffi-bindgen", + "--", + "--language", + "kotlin", + "--out-dir", + "../bdk-jvm/lib/src/main/kotlin" + ) + + doLast { + println("JVM bindings file successfully created") + } + } + + // we need an aggregate task which will run the 3 required tasks to build the JVM libs in order + // the task will also appear in the printout of the ./gradlew tasks task with a group and description + tasks.register("buildJvmLib") { + group = "Bitcoindevkit" + description = "Aggregate task to build JVM library" + + dependsOn( + buildJvmBinaries, + moveNativeJvmLibs, + generateJvmBindings + ) + } + } +} diff --git a/bdk-kotlin/bdk-jvm/settings.gradle.kts b/bdk-kotlin/bdk-jvm/settings.gradle.kts new file mode 100644 index 0000000..d4238f3 --- /dev/null +++ b/bdk-kotlin/bdk-jvm/settings.gradle.kts @@ -0,0 +1,4 @@ +rootProject.name = "bdk-jvm" + +include(":lib") +includeBuild("plugins")