Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68a8bee7d | ||
|
|
e250d4ae1f | ||
|
|
3e0ae31890 | ||
|
|
d197e17eaa | ||
|
|
63cbcb1aa3 | ||
|
|
be8b31684f | ||
|
|
862658ce96 | ||
|
|
6257911095 | ||
|
|
01da0137ef | ||
|
|
f86a9df594 | ||
|
|
379cbe0b59 | ||
|
|
4fd4a7ee6f | ||
|
|
c6c4446092 | ||
|
|
358cc35b60 | ||
|
|
c58a31f711 | ||
|
|
8a9e025e2f | ||
|
|
2ac26fa060 | ||
|
|
7e61659cb7 | ||
|
|
947a5cb8e0 | ||
|
|
f6b099aa76 | ||
|
|
0467e12aae | ||
|
|
94b07b9fb9 | ||
|
|
9ee31d97c7 | ||
|
|
2c30bdff56 | ||
|
|
97c59b4cad | ||
|
|
c5f18ca998 | ||
|
|
c79f1b6326 | ||
|
|
334bafcc7c | ||
|
|
1148a2e6d7 | ||
|
|
80510381de | ||
|
|
a4370332df | ||
|
|
693a75e6d1 | ||
|
|
b6fa3539e4 | ||
|
|
5e7a42df07 | ||
|
|
3b05c57794 | ||
|
|
aabb09ccb8 | ||
|
|
ab301f075b | ||
|
|
9681f30e19 | ||
|
|
ef73e38154 | ||
|
|
e98298090d | ||
|
|
d1b24026e6 | ||
|
|
4ad9b89e25 | ||
|
|
a8d9864825 | ||
|
|
71531b7abb | ||
|
|
a7a45a036c | ||
|
|
7e02dbc97a | ||
|
|
1b30ce14a3 | ||
|
|
e0937b73db | ||
|
|
a8b161569c | ||
|
|
04eec7bf56 | ||
|
|
f2bbe668b1 | ||
|
|
f1c2118b02 | ||
|
|
f34e59e289 | ||
|
|
892bfe868f | ||
|
|
fe251c12f3 | ||
|
|
ee59dbe543 | ||
|
|
2f83a9ed05 | ||
|
|
4a7d665f7c | ||
|
|
25b8a8841d | ||
|
|
38b8589526 | ||
|
|
3693e99372 | ||
|
|
d97a13d186 | ||
|
|
4e1c6bd62b | ||
|
|
04eee046cb | ||
|
|
69a676ba36 | ||
|
|
a528a76c5d | ||
|
|
653773a638 | ||
|
|
857d99030b | ||
|
|
2db59de659 | ||
|
|
c15993310d | ||
|
|
b2f2f9135d | ||
|
|
3a7e7baf51 | ||
|
|
d307b281d7 | ||
|
|
41afeafcd3 | ||
|
|
47651f3681 | ||
|
|
9b7a9ded56 | ||
|
|
178fd6f010 | ||
|
|
31710ac77b | ||
|
|
6d7939c88f | ||
|
|
598d08b3bc | ||
|
|
b6d6a0d092 | ||
|
|
6093a8750b | ||
|
|
279f304d5d | ||
|
|
64fbf60a6a | ||
|
|
ce93e4e954 | ||
|
|
cf56619157 | ||
|
|
caff93c945 | ||
|
|
01bfe5d10e | ||
|
|
091c9994fa | ||
|
|
f30558d55c | ||
|
|
4efbfc1e9c | ||
|
|
3491a4548d | ||
|
|
0cc2f0a5be | ||
|
|
784f754cd9 | ||
|
|
8faba58cd4 | ||
|
|
0b265a7c26 | ||
|
|
f9e3bdfdb2 | ||
|
|
39e5efe5c0 | ||
|
|
342c4c4aa8 | ||
|
|
bb9fdd35cf | ||
|
|
a1aea54c53 | ||
|
|
a33a09f2a3 | ||
|
|
6361b41f33 | ||
|
|
2abe7205cb | ||
|
|
3e31e9aca3 | ||
|
|
a056c0dd59 | ||
|
|
060e54a718 | ||
|
|
59fe24818a | ||
|
|
91ae8dd537 | ||
|
|
13e7217ffd | ||
|
|
af828efa93 | ||
|
|
0f7a4aebf8 | ||
|
|
9f29eb0e86 | ||
|
|
3a5d4816ac | ||
|
|
2b40718875 | ||
|
|
36333b9fb7 | ||
|
|
f6c10da805 | ||
|
|
76087b9aec | ||
|
|
43ddf0a3b1 | ||
|
|
5303de9593 | ||
|
|
333f694d55 | ||
|
|
308d4af4f1 | ||
|
|
2aad5d4428 | ||
|
|
f31ebaa1e6 | ||
|
|
43425c8875 | ||
|
|
cb54405aed | ||
|
|
49126a8943 | ||
|
|
ec9d2ea284 | ||
|
|
f07f0248ef | ||
|
|
d14ea9a059 |
29
.editorconfig
Normal file
29
.editorconfig
Normal file
@@ -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
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,4 +1,19 @@
|
||||
/target
|
||||
*.h
|
||||
/main
|
||||
/Cargo.lock
|
||||
target
|
||||
build
|
||||
Cargo.lock
|
||||
/bindings/bdk-kotlin/local.properties
|
||||
/bindings/bdk-swift
|
||||
/bindings/bdk-swift.swiftdoc
|
||||
/bindings/bdk-swift.swiftsourceinfo
|
||||
.gradle
|
||||
wallet_db
|
||||
bdk_ffi_test
|
||||
local.properties
|
||||
*.log
|
||||
*.dylib
|
||||
*.so
|
||||
.DS_Store
|
||||
testdb
|
||||
xcuserdata
|
||||
.lsp
|
||||
.clj-kondo
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "bdk_ffi"
|
||||
name = "bdk-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Steve Myers <steve@notmandatory.org>"]
|
||||
authors = ["Steve Myers <steve@notmandatory.org>", "Sudarsan Balaji <sudarsan.balaji@artfuldev.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
crate-type = ["staticlib", "cdylib"]
|
||||
name = "bdkffi"
|
||||
|
||||
[dependencies]
|
||||
bdk = { version = "^0.7", feature = ["all-keys"] }
|
||||
safer-ffi = { version = "*", features = ["proc_macros"]}
|
||||
bdk = { version = "0.13", features = ["all-keys", "use-esplora-ureq"] }
|
||||
uniffi_macros = "0.14.1"
|
||||
uniffi = "0.14.1"
|
||||
thiserror = "1.0"
|
||||
|
||||
[features]
|
||||
c-headers = ["safer-ffi/headers"]
|
||||
[build-dependencies]
|
||||
uniffi_build = "0.14.1"
|
||||
|
||||
112
README.md
Normal file
112
README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Foreign language bindings for BDK (bdk-ffi)
|
||||
|
||||
This repository contains source code for generating foreign language bindings
|
||||
for the rust library bdk for the Bitcoin Dev Kit (BDK) project.
|
||||
|
||||
## Supported target languages and platforms
|
||||
|
||||
| Language | Platform | Status |
|
||||
| --- | --- | --- |
|
||||
| Kotlin | JVM | WIP |
|
||||
| Kotlin | Android | WIP |
|
||||
| Swift | iOS | WIP |
|
||||
|
||||
|
||||
## Getting Started (User)
|
||||
|
||||
If you just want to consume the language bindings:
|
||||
|
||||
### Kotlin (JVM)
|
||||
|
||||
Just add the dependency `org.bitcoindevkit:bdk-jvm:0.1.1`. The package is `org.bitcoindevkit.bdk`.
|
||||
|
||||
### Kotlin (Android)
|
||||
|
||||
Just add the dependency `org.bitcoindevkit:bdk-android:0.1.1`. The package is `org.bitcoindevkit.bdk`.
|
||||
|
||||
## Getting Started (Developer)
|
||||
|
||||
This project uses rust. A basic knowledge of the rust ecosystem is helpful.
|
||||
|
||||
### General
|
||||
1. Install `uniffi-bindgen`
|
||||
```sh
|
||||
cargo install uniffi_bindgen
|
||||
```
|
||||
1. See the [UniFFI User Guide](https://mozilla.github.io/uniffi-rs/) for more info
|
||||
|
||||
### Kotlin Bindings for JVM (OSX / Linux)
|
||||
|
||||
1. Install required targets
|
||||
```sh
|
||||
rustup target add x86_64-apple-darwin x86_64-unknown-linux-gnu
|
||||
```
|
||||
1. Build kotlin (JVM) bindings
|
||||
```sh
|
||||
./build.sh -k
|
||||
```
|
||||
1. Generated kotlin bindings are available at `/bindings/bdk-kotlin/`
|
||||
1. A demo app is available at `/bindings/bdk-kotlin/demo/`. It uses stdin for
|
||||
inputs and can be run from gradle.
|
||||
```sh
|
||||
cd bindings/bdk-kotlin
|
||||
./gradlew :demo:run
|
||||
```
|
||||
|
||||
### Kotlin bindings for Android
|
||||
|
||||
1. Install required targets
|
||||
```sh
|
||||
rustup target add x86_64-linux-android aarch64-linux-android
|
||||
armv7-linux-androideabi i686-linux-android
|
||||
```
|
||||
1. Install Android SDK and Build-Tools for API level 30+
|
||||
1. Setup `$ANDROID_NDK_HOME` and `$ANDROID_SDK_ROOT` path variables (which are
|
||||
required by the build scripts)
|
||||
1. Build kotlin (Android) bindings
|
||||
```sh
|
||||
./build.sh -a
|
||||
```
|
||||
2. A demo android app is available at [notmandatory/bdk-sample-app](https://github.com/notmandatory/bitcoindevkit-android-sample-app/tree/upgrade-to-bdk-ffi/)
|
||||
|
||||
### Swift bindings for iOS
|
||||
|
||||
1. Install the latest version of xcode, download and install the advanced tools.
|
||||
1. Ensure Swift is installed
|
||||
1. Install required targets
|
||||
```sh
|
||||
rustup target add aarch64-apple-ios x86_64-apple-ios
|
||||
```
|
||||
1. Build swift (iOS) bindings
|
||||
```sh
|
||||
./build.sh -s
|
||||
```
|
||||
1. Example iOS app can be found in `/examples/iOS` which can be run by xcode.
|
||||
|
||||
## Notes
|
||||
|
||||
### Adding new structs and functions
|
||||
|
||||
See the [UniFFI User Guide](https://mozilla.github.io/uniffi-rs/)
|
||||
|
||||
#### For pass by value objects
|
||||
|
||||
1. create new rust struct with only fields that are supported UniFFI types
|
||||
1. update mapping `bdk.udl` file with new `dictionary`
|
||||
|
||||
#### For pass by reference values
|
||||
|
||||
1. create wrapper rust struct/impl with only fields that are `Sync + Send`
|
||||
1. update mapping `bdk.udl` file with new `interface`
|
||||
|
||||
## Goals
|
||||
|
||||
1. Language bindings should feel idiomatic in target languages/platforms
|
||||
1. Adding new targets should be easy
|
||||
1. Getting up and running should be easy
|
||||
1. Contributing should be easy
|
||||
1. Get it right, then automate
|
||||
|
||||
## Thanks
|
||||
|
||||
This project is made possible thanks to the wonderful work on [mozilla/uniffi-rs](https://github.com/mozilla/uniffi-rs)
|
||||
3
build.rs
Normal file
3
build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi_build::generate_scaffolding("src/bdk.udl").unwrap();
|
||||
}
|
||||
145
build.sh
Executable file
145
build.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# functions
|
||||
|
||||
## help
|
||||
help()
|
||||
{
|
||||
# Display Help
|
||||
echo "Build bdk-ffi and related libraries."
|
||||
echo
|
||||
echo "Syntax: build [-a|h|k|s]"
|
||||
echo "options:"
|
||||
echo "-a Android."
|
||||
echo "-h Print this Help."
|
||||
echo "-k Kotlin."
|
||||
echo "-s Swift."
|
||||
echo
|
||||
}
|
||||
|
||||
## rust
|
||||
build_rust() {
|
||||
echo "Build Rust library"
|
||||
cargo fmt
|
||||
cargo build
|
||||
cargo test
|
||||
}
|
||||
|
||||
## copy to bdk-bdk-kotlin
|
||||
copy_lib_kotlin() {
|
||||
echo -n "Copy "
|
||||
case $OS in
|
||||
"Darwin")
|
||||
echo -n "darwin "
|
||||
mkdir -p bindings/bdk-kotlin/jvm/src/main/resources/darwin-x86-64
|
||||
cp target/debug/libbdkffi.dylib bindings/bdk-kotlin/jvm/src/main/resources/darwin-x86-64
|
||||
;;
|
||||
"Linux")
|
||||
echo -n "linux "
|
||||
mkdir -p bindings/bdk-kotlin/jvm/src/main/resources/linux-x86-64
|
||||
cp target/debug/libbdkffi.so bindings/bdk-kotlin/jvm/src/main/resources/linux-x86-64
|
||||
;;
|
||||
esac
|
||||
echo "libs to kotlin sub-project"
|
||||
}
|
||||
|
||||
## bdk-bdk-kotlin jar
|
||||
build_kotlin() {
|
||||
copy_lib_kotlin
|
||||
uniffi-bindgen generate src/bdk.udl --no-format --out-dir bindings/bdk-kotlin/jvm/src/main/kotlin --language kotlin
|
||||
}
|
||||
|
||||
## bdk swift
|
||||
build_swift() {
|
||||
uniffi-bindgen generate src/bdk.udl --no-format --out-dir bindings/bdk-swift/ --language swift
|
||||
swiftc -module-name bdk -emit-library -o libbdkffi.dylib -emit-module -emit-module-path ./bindings/bdk-swift/ -parse-as-library -L ./target/debug/ -lbdkffi -Xcc -fmodule-map-file=./bindings/bdk-swift/bdkFFI.modulemap ./bindings/bdk-swift/bdk.swift
|
||||
TARGETDIR=target
|
||||
RELDIR=debug
|
||||
STATIC_LIB_NAME=libbdkffi.a
|
||||
|
||||
# We can't use cargo lipo because we can't link to universal libraries :(
|
||||
# https://github.com/rust-lang/rust/issues/55235
|
||||
LIBS_ARCHS=("x86_64" "arm64")
|
||||
IOS_TRIPLES=("x86_64-apple-ios" "aarch64-apple-ios")
|
||||
for i in "${!LIBS_ARCHS[@]}"; do
|
||||
cargo build --target "${IOS_TRIPLES[${i}]}"
|
||||
done
|
||||
|
||||
UNIVERSAL_BINARY=./${TARGETDIR}/ios/universal/${RELDIR}/${STATIC_LIB_NAME}
|
||||
NEED_LIPO=
|
||||
|
||||
# if the universal binary doesnt exist, or if it's older than the static libs,
|
||||
# we need to run `lipo` again.
|
||||
if [[ ! -f "${UNIVERSAL_BINARY}" ]]; then
|
||||
NEED_LIPO=1
|
||||
elif [[ "$(stat -f "%m" "./${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then
|
||||
NEED_LIPO=1
|
||||
elif [[ "$(stat -f "%m" "./${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then
|
||||
NEED_LIPO=1
|
||||
fi
|
||||
if [[ "${NEED_LIPO}" = "1" ]]; then
|
||||
mkdir -p "${TARGETDIR}/ios/universal/${RELDIR}"
|
||||
lipo -create -output "${UNIVERSAL_BINARY}" \
|
||||
"${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" \
|
||||
"${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}"
|
||||
fi
|
||||
}
|
||||
|
||||
## rust android
|
||||
build_android() {
|
||||
build_kotlin
|
||||
|
||||
# If ANDROID_NDK_HOME is not set then set it to github actions default
|
||||
[ -z "$ANDROID_NDK_HOME" ] && export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
||||
|
||||
# Update this line accordingly if you are not building *from* darwin-x86_64 or linux-x86_64
|
||||
export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/`uname | tr '[:upper:]' '[:lower:]'`-x86_64/bin
|
||||
|
||||
# Required for 'ring' dependency to cross-compile to Android platform, must be at least 21
|
||||
export CFLAGS="-D__ANDROID_API__=21"
|
||||
|
||||
# IMPORTANT: make sure every target is not a substring of a different one. We check for them with grep later on
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-aarch64,x86_64,i686}"
|
||||
|
||||
mkdir -p bindings/bdk-kotlin/android/src/main/jniLibs/ bindings/bdk-kotlin/android/src/main/jniLibs/arm64-v8a bindings/bdk-kotlin/android/src/main/jniLibs/x86_64 bindings/bdk-kotlin/android/src/main/jniLibs/x86
|
||||
|
||||
if echo $BUILD_TARGETS | grep "aarch64"; then
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --target=aarch64-linux-android
|
||||
cp target/aarch64-linux-android/debug/libbdkffi.so bindings/bdk-kotlin/android/src/main/jniLibs/arm64-v8a
|
||||
fi
|
||||
if echo $BUILD_TARGETS | grep "x86_64"; then
|
||||
CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --target=x86_64-linux-android
|
||||
cp target/x86_64-linux-android/debug/libbdkffi.so bindings/bdk-kotlin/android/src/main/jniLibs/x86_64
|
||||
fi
|
||||
if echo $BUILD_TARGETS | grep "i686"; then
|
||||
CARGO_TARGET_I686_LINUX_ANDROID_LINKER="i686-linux-android21-clang" CC="i686-linux-android21-clang" cargo build --target=i686-linux-android
|
||||
cp target/i686-linux-android/debug/libbdkffi.so bindings/bdk-kotlin/android/src/main/jniLibs/x86
|
||||
fi
|
||||
|
||||
# copy sources
|
||||
cp -R bindings/bdk-kotlin/jvm/src/main/kotlin bindings/bdk-kotlin/android/src/main
|
||||
|
||||
# bdk-kotlin aar
|
||||
(cd bindings/bdk-kotlin && ./gradlew :android:build)
|
||||
}
|
||||
|
||||
OS=$(uname)
|
||||
|
||||
if [ "$1" == "-h" ]
|
||||
then
|
||||
help
|
||||
else
|
||||
build_rust
|
||||
|
||||
while [ -n "$1" ]; do # while loop starts
|
||||
case "$1" in
|
||||
-a) build_android ;;
|
||||
-k) build_kotlin ;;
|
||||
-s) build_swift ;;
|
||||
-h) help ;;
|
||||
*) echo "Option $1 not recognized" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
fi
|
||||
13
main.c
13
main.c
@@ -1,13 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "bdk_ffi.h"
|
||||
|
||||
int main (int argc, char const * const argv[])
|
||||
{
|
||||
Point_t * a = new_point(84,45);
|
||||
Point_t * b = new_point(0.0,39.0);
|
||||
Point_t * m = mid_point(a, b);
|
||||
print_point(m);
|
||||
print_point(NULL);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
167
src/bdk.udl
Normal file
167
src/bdk.udl
Normal file
@@ -0,0 +1,167 @@
|
||||
namespace bdk {
|
||||
[Throws=BdkError]
|
||||
ExtendedKeyInfo generate_extended_key(Network network, MnemonicType mnemonic_type, string? password);
|
||||
[Throws=BdkError]
|
||||
ExtendedKeyInfo restore_extended_key(Network network, string mnemonic, string? password);
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum BdkError {
|
||||
"InvalidU32Bytes",
|
||||
"Generic",
|
||||
"ScriptDoesntHaveAddressForm",
|
||||
"NoRecipients",
|
||||
"NoUtxosSelected",
|
||||
"OutputBelowDustLimit",
|
||||
"InsufficientFunds",
|
||||
"BnBTotalTriesExceeded",
|
||||
"BnBNoExactMatch",
|
||||
"UnknownUtxo",
|
||||
"TransactionNotFound",
|
||||
"TransactionConfirmed",
|
||||
"IrreplaceableTransaction",
|
||||
"FeeRateTooLow",
|
||||
"FeeTooLow",
|
||||
"FeeRateUnavailable",
|
||||
"MissingKeyOrigin",
|
||||
"Key",
|
||||
"ChecksumMismatch",
|
||||
"SpendingPolicyRequired",
|
||||
"InvalidPolicyPathError",
|
||||
"Signer",
|
||||
"InvalidNetwork",
|
||||
"InvalidProgressValue",
|
||||
"ProgressUpdateError",
|
||||
"InvalidOutpoint",
|
||||
"Descriptor",
|
||||
"AddressValidator",
|
||||
"Encode",
|
||||
"Miniscript",
|
||||
"Bip32",
|
||||
"Secp256k1",
|
||||
"Json",
|
||||
"Hex",
|
||||
"Psbt",
|
||||
"PsbtParse",
|
||||
"Electrum",
|
||||
"Esplora",
|
||||
"Sled",
|
||||
};
|
||||
|
||||
enum Network {
|
||||
"Bitcoin",
|
||||
"Testnet",
|
||||
"Signet",
|
||||
"Regtest",
|
||||
};
|
||||
|
||||
dictionary SledDbConfiguration {
|
||||
string path;
|
||||
string tree_name;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface DatabaseConfig {
|
||||
Memory(string junk);
|
||||
Sled(SledDbConfiguration config);
|
||||
};
|
||||
|
||||
dictionary TransactionDetails {
|
||||
u64? fees;
|
||||
u64 received;
|
||||
u64 sent;
|
||||
string id;
|
||||
};
|
||||
|
||||
dictionary Confirmation {
|
||||
u32 height;
|
||||
u64 timestamp;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface Transaction {
|
||||
Unconfirmed(TransactionDetails details);
|
||||
Confirmed(TransactionDetails details, Confirmation confirmation);
|
||||
};
|
||||
|
||||
interface OfflineWallet {
|
||||
[Throws=BdkError]
|
||||
constructor(string descriptor, Network network, DatabaseConfig database_config);
|
||||
|
||||
// OfflineWalletOperations
|
||||
string get_new_address();
|
||||
string get_last_unused_address();
|
||||
[Throws=BdkError]
|
||||
u64 get_balance();
|
||||
[Throws=BdkError]
|
||||
void sign([ByRef] PartiallySignedBitcoinTransaction psbt);
|
||||
[Throws=BdkError]
|
||||
sequence<Transaction> get_transactions();
|
||||
};
|
||||
|
||||
dictionary ElectrumConfig {
|
||||
string url;
|
||||
string? socks5;
|
||||
u8 retry;
|
||||
u8? timeout;
|
||||
u64 stop_gap;
|
||||
};
|
||||
|
||||
dictionary EsploraConfig {
|
||||
string base_url;
|
||||
string? proxy;
|
||||
u64 timeout_read;
|
||||
u64 timeout_write;
|
||||
u64 stop_gap;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface BlockchainConfig {
|
||||
Electrum(ElectrumConfig config);
|
||||
Esplora(EsploraConfig config);
|
||||
};
|
||||
|
||||
callback interface BdkProgress {
|
||||
void update(f32 progress, string? message);
|
||||
};
|
||||
|
||||
interface OnlineWallet {
|
||||
[Throws=BdkError]
|
||||
constructor(string descriptor, string? change_descriptor, Network network, DatabaseConfig database_config, BlockchainConfig blockchain_config);
|
||||
|
||||
// OfflineWalletOperations
|
||||
string get_new_address();
|
||||
string get_last_unused_address();
|
||||
[Throws=BdkError]
|
||||
u64 get_balance();
|
||||
[Throws=BdkError]
|
||||
void sign([ByRef] PartiallySignedBitcoinTransaction psbt);
|
||||
[Throws=BdkError]
|
||||
sequence<Transaction> get_transactions();
|
||||
|
||||
// OnlineWalletInterface
|
||||
Network get_network();
|
||||
[Throws=BdkError]
|
||||
void sync(BdkProgress progress_update, u32? max_address_param);
|
||||
[Throws=BdkError]
|
||||
Transaction broadcast([ByRef] PartiallySignedBitcoinTransaction psbt);
|
||||
};
|
||||
|
||||
interface PartiallySignedBitcoinTransaction {
|
||||
[Throws=BdkError]
|
||||
constructor([ByRef] OnlineWallet wallet, string recipient, u64 amount, float? fee_rate);
|
||||
};
|
||||
|
||||
dictionary ExtendedKeyInfo {
|
||||
string mnemonic;
|
||||
string xprv;
|
||||
string fingerprint;
|
||||
};
|
||||
|
||||
enum MnemonicType {
|
||||
"Words12",
|
||||
"Words15",
|
||||
"Words18",
|
||||
"Words21",
|
||||
"Words24",
|
||||
};
|
||||
371
src/lib.rs
371
src/lib.rs
@@ -1,47 +1,340 @@
|
||||
use ::safer_ffi::prelude::*;
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bdk::bitcoin::{Address, Network};
|
||||
use bdk::blockchain::any::{AnyBlockchain, AnyBlockchainConfig};
|
||||
use bdk::blockchain::Progress;
|
||||
use bdk::blockchain::{
|
||||
electrum::ElectrumBlockchainConfig, esplora::EsploraBlockchainConfig, ConfigurableBlockchain,
|
||||
};
|
||||
use bdk::database::any::{AnyDatabase, SledDbConfiguration};
|
||||
use bdk::database::{AnyDatabaseConfig, ConfigurableDatabase};
|
||||
use bdk::keys::bip39::{Language, Mnemonic, MnemonicType};
|
||||
use bdk::keys::{DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::BareCtx;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::{ConfirmationTime, Error, FeeRate, SignOptions, Wallet};
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
/// A `struct` usable from both Rust and C
|
||||
#[derive_ReprC]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Point {
|
||||
x: f64,
|
||||
y: f64,
|
||||
uniffi_macros::include_scaffolding!("bdk");
|
||||
|
||||
type BdkError = Error;
|
||||
|
||||
pub enum DatabaseConfig {
|
||||
Memory { junk: String },
|
||||
Sled { config: SledDbConfiguration },
|
||||
}
|
||||
|
||||
/* Export a Rust function to the C world. */
|
||||
/// Returns the middle point of `[a, b]`.
|
||||
#[ffi_export]
|
||||
fn mid_point(a: Option<repr_c::Box<Point>>, b: Option<repr_c::Box<Point>>) -> repr_c::Box<Point> {
|
||||
let a = a.unwrap();
|
||||
let b = b.unwrap();
|
||||
repr_c::Box::new(Point {
|
||||
x: (a.x + b.x) / 2.,
|
||||
y: (a.y + b.y) / 2.,
|
||||
pub struct ElectrumConfig {
|
||||
pub url: String,
|
||||
pub socks5: Option<String>,
|
||||
pub retry: u8,
|
||||
pub timeout: Option<u8>,
|
||||
pub stop_gap: u64,
|
||||
}
|
||||
|
||||
pub struct EsploraConfig {
|
||||
pub base_url: String,
|
||||
pub proxy: Option<String>,
|
||||
pub timeout_read: u64,
|
||||
pub timeout_write: u64,
|
||||
pub stop_gap: u64,
|
||||
}
|
||||
|
||||
pub enum BlockchainConfig {
|
||||
Electrum { config: ElectrumConfig },
|
||||
Esplora { config: EsploraConfig },
|
||||
}
|
||||
|
||||
trait WalletHolder<B> {
|
||||
fn get_wallet(&self) -> MutexGuard<Wallet<B, AnyDatabase>>;
|
||||
}
|
||||
|
||||
struct OfflineWallet {
|
||||
wallet: Mutex<Wallet<(), AnyDatabase>>,
|
||||
}
|
||||
|
||||
impl WalletHolder<()> for OfflineWallet {
|
||||
fn get_wallet(&self) -> MutexGuard<Wallet<(), AnyDatabase>> {
|
||||
self.wallet.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TransactionDetails {
|
||||
pub fees: Option<u64>,
|
||||
pub received: u64,
|
||||
pub sent: u64,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
type Confirmation = ConfirmationTime;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Transaction {
|
||||
Unconfirmed {
|
||||
details: TransactionDetails,
|
||||
},
|
||||
Confirmed {
|
||||
details: TransactionDetails,
|
||||
confirmation: Confirmation,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&bdk::TransactionDetails> for TransactionDetails {
|
||||
fn from(x: &bdk::TransactionDetails) -> TransactionDetails {
|
||||
TransactionDetails {
|
||||
fees: x.fee,
|
||||
id: x.txid.to_string(),
|
||||
received: x.received,
|
||||
sent: x.sent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&bdk::TransactionDetails> for Transaction {
|
||||
fn from(x: &bdk::TransactionDetails) -> Transaction {
|
||||
match x.confirmation_time.clone() {
|
||||
Some(confirmation) => Transaction::Confirmed {
|
||||
details: TransactionDetails::from(x),
|
||||
confirmation,
|
||||
},
|
||||
None => Transaction::Unconfirmed {
|
||||
details: TransactionDetails::from(x),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait OfflineWalletOperations<B>: WalletHolder<B> {
|
||||
fn get_new_address(&self) -> String {
|
||||
self.get_wallet()
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_last_unused_address(&self) -> String {
|
||||
self.get_wallet()
|
||||
.get_address(AddressIndex::LastUnused)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_balance(&self) -> Result<u64, Error> {
|
||||
self.get_wallet().get_balance()
|
||||
}
|
||||
|
||||
fn sign<'a>(&self, psbt: &'a PartiallySignedBitcoinTransaction) -> Result<(), Error> {
|
||||
let mut psbt = psbt.internal.lock().unwrap();
|
||||
let finalized = self.get_wallet().sign(&mut psbt, SignOptions::default())?;
|
||||
match finalized {
|
||||
true => Ok(()),
|
||||
false => Err(BdkError::Generic(format!(
|
||||
"transaction signing not finalized {:?}",
|
||||
psbt
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_transactions(&self) -> Result<Vec<Transaction>, Error> {
|
||||
let transactions = self.get_wallet().list_transactions(true)?;
|
||||
Ok(transactions.iter().map(Transaction::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl OfflineWallet {
|
||||
fn new(
|
||||
descriptor: String,
|
||||
network: Network,
|
||||
database_config: DatabaseConfig,
|
||||
) -> Result<Self, BdkError> {
|
||||
let any_database_config = match database_config {
|
||||
DatabaseConfig::Memory { .. } => AnyDatabaseConfig::Memory(()),
|
||||
DatabaseConfig::Sled { config } => AnyDatabaseConfig::Sled(config),
|
||||
};
|
||||
let database = AnyDatabase::from_config(&any_database_config)?;
|
||||
let wallet = Mutex::new(Wallet::new_offline(&descriptor, None, network, database)?);
|
||||
Ok(OfflineWallet { wallet })
|
||||
}
|
||||
}
|
||||
|
||||
impl OfflineWalletOperations<()> for OfflineWallet {}
|
||||
|
||||
struct OnlineWallet {
|
||||
wallet: Mutex<Wallet<AnyBlockchain, AnyDatabase>>,
|
||||
}
|
||||
|
||||
pub trait BdkProgress: Send + Sync {
|
||||
fn update(&self, progress: f32, message: Option<String>);
|
||||
}
|
||||
|
||||
struct BdkProgressHolder {
|
||||
progress_update: Box<dyn BdkProgress>,
|
||||
}
|
||||
|
||||
impl Progress for BdkProgressHolder {
|
||||
fn update(&self, progress: f32, message: Option<String>) -> Result<(), Error> {
|
||||
self.progress_update.update(progress, message);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct PartiallySignedBitcoinTransaction {
|
||||
internal: Mutex<PartiallySignedTransaction>,
|
||||
details: bdk::TransactionDetails,
|
||||
}
|
||||
|
||||
impl PartiallySignedBitcoinTransaction {
|
||||
fn new(
|
||||
online_wallet: &OnlineWallet,
|
||||
recipient: String,
|
||||
amount: u64,
|
||||
fee_rate: Option<f32>, // satoshis per vbyte
|
||||
) -> Result<Self, Error> {
|
||||
let wallet = online_wallet.get_wallet();
|
||||
match Address::from_str(&recipient) {
|
||||
Ok(address) => {
|
||||
let (psbt, details) = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(address.script_pubkey(), amount);
|
||||
if let Some(sat_per_vb) = fee_rate {
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(sat_per_vb));
|
||||
}
|
||||
builder.finish()?
|
||||
};
|
||||
Ok(PartiallySignedBitcoinTransaction {
|
||||
internal: Mutex::new(psbt),
|
||||
details,
|
||||
})
|
||||
}
|
||||
Err(..) => Err(BdkError::Generic(
|
||||
"failed to read wallet address".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnlineWallet {
|
||||
fn new(
|
||||
descriptor: String,
|
||||
change_descriptor: Option<String>,
|
||||
network: Network,
|
||||
database_config: DatabaseConfig,
|
||||
blockchain_config: BlockchainConfig,
|
||||
) -> Result<Self, BdkError> {
|
||||
let any_database_config = match database_config {
|
||||
DatabaseConfig::Memory { .. } => AnyDatabaseConfig::Memory(()),
|
||||
DatabaseConfig::Sled { config } => AnyDatabaseConfig::Sled(config),
|
||||
};
|
||||
let any_blockchain_config = match blockchain_config {
|
||||
BlockchainConfig::Electrum { config } => {
|
||||
AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
|
||||
retry: config.retry,
|
||||
socks5: config.socks5,
|
||||
timeout: config.timeout,
|
||||
url: config.url,
|
||||
stop_gap: usize::try_from(config.stop_gap).unwrap(),
|
||||
})
|
||||
}
|
||||
BlockchainConfig::Esplora { config } => {
|
||||
AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
|
||||
base_url: config.base_url,
|
||||
proxy: config.proxy,
|
||||
timeout_read: config.timeout_read,
|
||||
timeout_write: config.timeout_write,
|
||||
stop_gap: usize::try_from(config.stop_gap).unwrap(),
|
||||
})
|
||||
}
|
||||
};
|
||||
let database = AnyDatabase::from_config(&any_database_config)?;
|
||||
let blockchain = AnyBlockchain::from_config(&any_blockchain_config)?;
|
||||
let wallet = Mutex::new(Wallet::new(
|
||||
&descriptor,
|
||||
change_descriptor.to_owned().as_ref(),
|
||||
network,
|
||||
database,
|
||||
blockchain,
|
||||
)?);
|
||||
Ok(OnlineWallet { wallet })
|
||||
}
|
||||
|
||||
fn get_network(&self) -> Network {
|
||||
self.wallet.lock().unwrap().network()
|
||||
}
|
||||
|
||||
fn sync(
|
||||
&self,
|
||||
progress_update: Box<dyn BdkProgress>,
|
||||
max_address_param: Option<u32>,
|
||||
) -> Result<(), BdkError> {
|
||||
progress_update.update(21.0, Some("message".to_string()));
|
||||
self.wallet
|
||||
.lock()
|
||||
.unwrap()
|
||||
.sync(BdkProgressHolder { progress_update }, max_address_param)
|
||||
}
|
||||
|
||||
fn broadcast<'a>(
|
||||
&self,
|
||||
psbt: &'a PartiallySignedBitcoinTransaction,
|
||||
) -> Result<Transaction, Error> {
|
||||
let tx = psbt.internal.lock().unwrap().clone().extract_tx();
|
||||
self.get_wallet().broadcast(tx)?;
|
||||
Ok(Transaction::from(&psbt.details))
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletHolder<AnyBlockchain> for OnlineWallet {
|
||||
fn get_wallet(&self) -> MutexGuard<Wallet<AnyBlockchain, AnyDatabase>> {
|
||||
self.wallet.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl OfflineWalletOperations<AnyBlockchain> for OnlineWallet {}
|
||||
|
||||
pub struct ExtendedKeyInfo {
|
||||
mnemonic: String,
|
||||
xprv: String,
|
||||
fingerprint: String,
|
||||
}
|
||||
|
||||
fn generate_extended_key(
|
||||
network: Network,
|
||||
mnemonic_type: MnemonicType,
|
||||
password: Option<String>,
|
||||
) -> Result<ExtendedKeyInfo, Error> {
|
||||
let mnemonic: GeneratedKey<_, BareCtx> =
|
||||
Mnemonic::generate((mnemonic_type, Language::English)).unwrap();
|
||||
let mnemonic = mnemonic.into_key();
|
||||
let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
|
||||
let xprv = xkey.into_xprv(network).unwrap();
|
||||
let fingerprint = xprv.fingerprint(&Secp256k1::new());
|
||||
Ok(ExtendedKeyInfo {
|
||||
mnemonic: mnemonic.to_string(),
|
||||
xprv: xprv.to_string(),
|
||||
fingerprint: fingerprint.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Pretty-prints a point using Rust's formatting logic.
|
||||
#[ffi_export]
|
||||
fn print_point(point: Option<repr_c::Box<Point>>) {
|
||||
println!("{:?}", point);
|
||||
fn restore_extended_key(
|
||||
network: Network,
|
||||
mnemonic: String,
|
||||
password: Option<String>,
|
||||
) -> Result<ExtendedKeyInfo, Error> {
|
||||
let mnemonic = Mnemonic::from_phrase(mnemonic.as_ref(), Language::English).unwrap();
|
||||
let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
|
||||
let xprv = xkey.into_xprv(network).unwrap();
|
||||
let fingerprint = xprv.fingerprint(&Secp256k1::new());
|
||||
Ok(ExtendedKeyInfo {
|
||||
mnemonic: mnemonic.to_string(),
|
||||
xprv: xprv.to_string(),
|
||||
fingerprint: fingerprint.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[ffi_export]
|
||||
fn new_point(x: f64, y: f64) -> repr_c::Box<Point> {
|
||||
repr_c::Box::new(Point { x, y })
|
||||
}
|
||||
|
||||
#[ffi_export]
|
||||
fn free_point(point: Option<repr_c::Box<Point>>) {
|
||||
drop(point)
|
||||
}
|
||||
|
||||
/// The following test function is necessary for the header generation.
|
||||
#[::safer_ffi::cfg_headers]
|
||||
#[test]
|
||||
fn generate_headers() -> ::std::io::Result<()> {
|
||||
::safer_ffi::headers::builder()
|
||||
.to_file("bdk_ffi.h")?
|
||||
.generate()
|
||||
}
|
||||
uniffi::deps::static_assertions::assert_impl_all!(OfflineWallet: Sync, Send);
|
||||
uniffi::deps::static_assertions::assert_impl_all!(OnlineWallet: Sync, Send);
|
||||
|
||||
44
test.sh
Executable file
44
test.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# functions
|
||||
|
||||
## help
|
||||
help()
|
||||
{
|
||||
# Display Help
|
||||
echo "Test bdk-uniffi and related libraries."
|
||||
echo
|
||||
echo "Syntax: build [-a|h|k]"
|
||||
echo "options:"
|
||||
echo "-a Android connected device tests."
|
||||
echo "-h Print this Help."
|
||||
echo "-k Kotlin tests."
|
||||
echo
|
||||
}
|
||||
|
||||
test_kotlin() {
|
||||
(cd bindings/bdk-kotlin && ./gradlew :jvm:test -Djna.debug_load=true)
|
||||
}
|
||||
|
||||
test_android() {
|
||||
(cd bindings/bdk-kotlin && ./gradlew :android:connectedDebugAndroidTest)
|
||||
}
|
||||
|
||||
if [ $1 = "-h" ]
|
||||
then
|
||||
help
|
||||
else
|
||||
cargo test
|
||||
|
||||
# optional tests
|
||||
while [ -n "$1" ]; do # while loop starts
|
||||
case "$1" in
|
||||
-a) test_android ;;
|
||||
-h) help ;;
|
||||
-k) test_kotlin ;;
|
||||
*) echo "Option $1 not recognized" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
fi
|
||||
12
uniffi.toml
Normal file
12
uniffi.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.bitcoindevkit"
|
||||
cdylib_name = "bdkffi"
|
||||
|
||||
[bindings.python]
|
||||
cdylib_name = "bdkffi"
|
||||
|
||||
[bindings.ruby]
|
||||
cdylib_name = "bdkffi"
|
||||
|
||||
[bindings.swift]
|
||||
cdylib_name = "bdkffi"
|
||||
Reference in New Issue
Block a user