From 091c9994fae15c00a4401880c4c28e44148a2169 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 12 Oct 2021 15:24:11 -0700 Subject: [PATCH] [WIP] reorganize and remove old stuff --- .gitignore | 2 +- Cargo.toml | 2 +- README.md | 6 +- build.sh | 28 +- src/error.rs | 134 --------- src/types.rs | 40 --- src/uniffi/bdk/bdk.kt | 602 -------------------------------------- src/wallet/blockchain.rs | 106 ------- src/wallet/database.rs | 32 -- src/wallet/mod.rs | 175 ----------- src/wallet/transaction.rs | 124 -------- 11 files changed, 19 insertions(+), 1232 deletions(-) delete mode 100644 src/error.rs delete mode 100644 src/types.rs delete mode 100644 src/uniffi/bdk/bdk.kt delete mode 100644 src/wallet/blockchain.rs delete mode 100644 src/wallet/database.rs delete mode 100644 src/wallet/mod.rs delete mode 100644 src/wallet/transaction.rs diff --git a/.gitignore b/.gitignore index 66ded16..2a8baf6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ wallet_db bdk_ffi_test local.properties *.log -targets/kotlin/testdb \ No newline at end of file +*.dylib \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 35d42a7..f6dc768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi_bdk" version = "0.1.0" -authors = ["Steve Myers "] +authors = ["Steve Myers ", ""] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 41a4d19..e363d70 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ UniFFI 1. cargo install uniffi_bindgen 2. cargo build -3. uniffi-bindgen generate --no-format --out-dir targets/kotlin/src/main/kotlin src/bdk.udl --language kotlin -4. cp target/debug/libuniffi_bdk.dylib targets/kotlin/src/main/resources/darwin-x86-64 -5. gradle build -Djna.debug_load=true -Djna.debug_load.jna +3. uniffi-bindgen generate --no-format --out-dir bindings/bdk-kotlin/src/main/kotlin src/bdk.udl --language kotlin +4. cp target/debug/libuniffi_bdk.dylib bindings/bdk-kotlin/src/main/resources/darwin-x86-64 +5. cd bindings/bdk-kotlin; gradle build -Djna.debug_load=true -Djna.debug_load.jna Setup Android build environment diff --git a/build.sh b/build.sh index 2ccb2a7..32f6369 100755 --- a/build.sh +++ b/build.sh @@ -32,27 +32,27 @@ build_cc() { cc cc/bdk_ffi_test.c -o cc/bdk_ffi_test -L target/debug -l bdk_ffi -l pthread -l dl -l m } -## copy to bdk-kotlin +## copy to bdk-bdk-kotlin copy_lib_kotlin() { echo -n "Copy " case $OS in "Darwin") echo -n "darwin " - mkdir -p bdk-kotlin/jvm/src/main/resources/darwin-x86-64 - cp target/debug/libbdk_ffi.dylib bdk-kotlin/jvm/src/main/resources/darwin-x86-64 + mkdir -p bdk-bdk-kotlin/jvm/src/main/resources/darwin-x86-64 + cp target/debug/libbdk_ffi.dylib bdk-bdk-kotlin/jvm/src/main/resources/darwin-x86-64 ;; "Linux") echo -n "linux " - mkdir -p bdk-kotlin/jvm/src/main/resources/linux-x86-64 - cp target/debug/libbdk_ffi.so bdk-kotlin/jvm/src/main/resources/linux-x86-64 + mkdir -p bdk-bdk-kotlin/jvm/src/main/resources/linux-x86-64 + cp target/debug/libbdk_ffi.so bdk-bdk-kotlin/jvm/src/main/resources/linux-x86-64 ;; esac echo "libs to kotlin sub-project" } -## bdk-kotlin jar +## bdk-bdk-kotlin jar build_kotlin() { - (cd bdk-kotlin && ./gradlew :jvm:build && ./gradlew :jvm:publishToMavenLocal) + (cd bdk-bdk-kotlin && ./gradlew :jvm:build && ./gradlew :jvm:publishToMavenLocal) } ## rust android @@ -69,27 +69,27 @@ build_android() { # 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,armv7,x86_64,i686}" - mkdir -p bdk-kotlin/android/src/main/jniLibs/ bdk-kotlin/android/src/main/jniLibs/arm64-v8a bdk-kotlin/android/src/main/jniLibs/x86_64 bdk-kotlin/android/src/main/jniLibs/armeabi-v7a bdk-kotlin/android/src/main/jniLibs/x86 + mkdir -p bdk-bdk-kotlin/android/src/main/jniLibs/ bdk-bdk-kotlin/android/src/main/jniLibs/arm64-v8a bdk-bdk-kotlin/android/src/main/jniLibs/x86_64 bdk-bdk-kotlin/android/src/main/jniLibs/armeabi-v7a bdk-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/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/arm64-v8a + cp target/aarch64-linux-android/debug/libbdk_ffi.so bdk-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/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86_64 + cp target/x86_64-linux-android/debug/libbdk_ffi.so bdk-bdk-kotlin/android/src/main/jniLibs/x86_64 fi if echo $BUILD_TARGETS | grep "armv7"; then CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --target=armv7-linux-androideabi - cp target/armv7-linux-androideabi/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/armeabi-v7a + cp target/armv7-linux-androideabi/debug/libbdk_ffi.so bdk-bdk-kotlin/android/src/main/jniLibs/armeabi-v7a 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/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86 + cp target/i686-linux-android/debug/libbdk_ffi.so bdk-bdk-kotlin/android/src/main/jniLibs/x86 fi - # bdk-kotlin aar - (cd bdk-kotlin && ./gradlew :android:build && ./gradlew :android:publishToMavenLocal) + # bdk-bdk-kotlin aar + (cd bdk-bdk-kotlin && ./gradlew :android:build && ./gradlew :android:publishToMavenLocal) } OS=$(uname) diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 737ff62..0000000 --- a/src/error.rs +++ /dev/null @@ -1,134 +0,0 @@ -use bdk::Error; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum FfiError { - #[error("data store disconnected")] - None, - #[error("data store disconnected")] - InvalidU32Bytes, - #[error("data store disconnected")] - Generic, - #[error("data store disconnected")] - ScriptDoesntHaveAddressForm, - #[error("data store disconnected")] - NoRecipients, - #[error("data store disconnected")] - NoUtxosSelected, - #[error("data store disconnected")] - OutputBelowDustLimit, - #[error("data store disconnected")] - InsufficientFunds, - #[error("data store disconnected")] - BnBTotalTriesExceeded, - #[error("data store disconnected")] - BnBNoExactMatch, - #[error("data store disconnected")] - UnknownUtxo, - #[error("data store disconnected")] - TransactionNotFound, - #[error("data store disconnected")] - TransactionConfirmed, - #[error("data store disconnected")] - IrreplaceableTransaction, - #[error("data store disconnected")] - FeeRateTooLow, - #[error("data store disconnected")] - FeeTooLow, - #[error("data store disconnected")] - FeeRateUnavailable, - #[error("data store disconnected")] - MissingKeyOrigin, - #[error("data store disconnected")] - Key, - #[error("data store disconnected")] - ChecksumMismatch, - #[error("data store disconnected")] - SpendingPolicyRequired, - #[error("data store disconnected")] - InvalidPolicyPathError, - #[error("data store disconnected")] - Signer, - #[error("data store disconnected")] - InvalidNetwork, - #[error("data store disconnected")] - InvalidProgressValue, - #[error("data store disconnected")] - ProgressUpdateError, - #[error("data store disconnected")] - InvalidOutpoint, - #[error("data store disconnected")] - Descriptor, - #[error("data store disconnected")] - AddressValidator, - #[error("data store disconnected")] - Encode, - #[error("data store disconnected")] - Miniscript, - #[error("data store disconnected")] - Bip32, - #[error("data store disconnected")] - Secp256k1, - #[error("data store disconnected")] - Json, - #[error("data store disconnected")] - Hex, - #[error("data store disconnected")] - Psbt, - #[error("data store disconnected")] - PsbtParse, - #[error("data store disconnected")] - Electrum, - // Esplora, - // CompactFilters, - #[error("data store disconnected")] - Sled, -} - -impl From for FfiError { - fn from(error: bdk::Error) -> Self { - match error { - Error::InvalidU32Bytes(_) => FfiError::InvalidU32Bytes, - Error::Generic(_) => FfiError::Generic, - Error::ScriptDoesntHaveAddressForm => FfiError::ScriptDoesntHaveAddressForm, - Error::NoRecipients => FfiError::NoRecipients, - Error::NoUtxosSelected => FfiError::NoUtxosSelected, - Error::OutputBelowDustLimit(_) => FfiError::OutputBelowDustLimit, - Error::InsufficientFunds { .. } => FfiError::InsufficientFunds, - Error::BnBTotalTriesExceeded => FfiError::BnBTotalTriesExceeded, - Error::BnBNoExactMatch => FfiError::BnBNoExactMatch, - Error::UnknownUtxo => FfiError::UnknownUtxo, - Error::TransactionNotFound => FfiError::TransactionNotFound, - Error::TransactionConfirmed => FfiError::TransactionConfirmed, - Error::IrreplaceableTransaction => FfiError::IrreplaceableTransaction, - Error::FeeRateTooLow { .. } => FfiError::FeeRateTooLow, - Error::FeeTooLow { .. } => FfiError::FeeTooLow, - Error::FeeRateUnavailable => FfiError::FeeRateUnavailable, - Error::MissingKeyOrigin(_) => FfiError::MissingKeyOrigin, - Error::Key(_) => FfiError::Key, - Error::ChecksumMismatch => FfiError::ChecksumMismatch, - Error::SpendingPolicyRequired(_) => FfiError::SpendingPolicyRequired, - Error::InvalidPolicyPathError(_) => FfiError::InvalidPolicyPathError, - Error::Signer(_) => FfiError::Signer, - Error::InvalidNetwork { .. } => FfiError::InvalidNetwork, - Error::InvalidProgressValue(_) => FfiError::InvalidProgressValue, - Error::ProgressUpdateError => FfiError::ProgressUpdateError, - Error::InvalidOutpoint(_) => FfiError::InvalidOutpoint, - Error::Descriptor(_) => FfiError::Descriptor, - Error::AddressValidator(_) => FfiError::AddressValidator, - Error::Encode(_) => FfiError::Encode, - Error::Miniscript(_) => FfiError::Miniscript, - Error::Bip32(_) => FfiError::Bip32, - Error::Secp256k1(_) => FfiError::Secp256k1, - Error::Json(_) => FfiError::Json, - Error::Hex(_) => FfiError::Hex, - Error::Psbt(_) => FfiError::Psbt, - Error::PsbtParse(_) => FfiError::PsbtParse, - Error::Electrum(_) => FfiError::Electrum, - // Error::Esplora(_) => JniError::Esplora, - // Error::CompactFilters(_) => JniError::CompactFilters, - // Error::Rpc(_) => JniError::Rpc, - Error::Sled(_) => FfiError::Sled, - } - } -} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 241cb42..0000000 --- a/src/types.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::error::FfiError; -use ::safer_ffi::prelude::*; -use safer_ffi::char_p::char_p_boxed; - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug)] -pub struct FfiResult { - pub ok: T, - pub err: FfiError, -} - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug)] -pub struct FfiResultVoid { - pub err: FfiError, -} - -#[ffi_export] -fn free_string_result(string_result: FfiResult) { - drop(string_result) -} - -#[ffi_export] -fn free_void_result(void_result: FfiResultVoid) { - drop(void_result) -} - -#[ffi_export] -fn free_uint64_result(void_result: FfiResult) { - drop(void_result) -} - -// TODO do we need this? remove? -/// Free a Rust-allocated string -#[ffi_export] -fn free_string(string: Option) { - drop(string) -} diff --git a/src/uniffi/bdk/bdk.kt b/src/uniffi/bdk/bdk.kt deleted file mode 100644 index eedd389..0000000 --- a/src/uniffi/bdk/bdk.kt +++ /dev/null @@ -1,602 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -@file:Suppress("NAME_SHADOWING") - -package uniffi.bdk; - -// Common helper code. -// -// Ideally this would live in a separate .kt file where it can be unittested etc -// in isolation, and perhaps even published as a re-useable package. -// -// However, it's important that the detils of how this helper code works (e.g. the -// way that different builtin types are passed across the FFI) exactly match what's -// expected by the Rust code on the other side of the interface. In practice right -// now that means coming from the exact some version of `uniffi` that was used to -// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin -// helpers directly inline like we're doing here. - -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.Pointer -import com.sun.jna.Structure -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock - -// This is a helper for safely working with byte buffers returned from the Rust code. -// A rust-owned buffer is represented by its capacity, its current length, and a -// pointer to the underlying data. - -@Structure.FieldOrder("capacity", "len", "data") -open class RustBuffer : Structure() { - @JvmField var capacity: Int = 0 - @JvmField var len: Int = 0 - @JvmField var data: Pointer? = null - - class ByValue : RustBuffer(), Structure.ByValue - class ByReference : RustBuffer(), Structure.ByReference - - companion object { - internal fun alloc(size: Int = 0) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_ed55_rustbuffer_alloc(size, status) - } - - internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_ed55_rustbuffer_free(buf, status) - } - - internal fun reserve(buf: RustBuffer.ByValue, additional: Int) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_ed55_rustbuffer_reserve(buf, additional, status) - } - } - - @Suppress("TooGenericExceptionThrown") - fun asByteBuffer() = - this.data?.getByteBuffer(0, this.len.toLong())?.also { - it.order(ByteOrder.BIG_ENDIAN) - } -} - -// This is a helper for safely passing byte references into the rust code. -// It's not actually used at the moment, because there aren't many things that you -// can take a direct pointer to in the JVM, and if we're going to copy something -// then we might as well copy it into a `RustBuffer`. But it's here for API -// completeness. - -@Structure.FieldOrder("len", "data") -open class ForeignBytes : Structure() { - @JvmField var len: Int = 0 - @JvmField var data: Pointer? = null - - class ByValue : ForeignBytes(), Structure.ByValue -} - - -// A helper for structured writing of data into a `RustBuffer`. -// This is very similar to `java.nio.ByteBuffer` but it knows how to grow -// the underlying `RustBuffer` on demand. -// -// TODO: we should benchmark writing things into a `RustBuffer` versus building -// up a bytearray and then copying it across. - -class RustBufferBuilder() { - var rbuf = RustBuffer.ByValue() - var bbuf: ByteBuffer? = null - - init { - val rbuf = RustBuffer.alloc(16) // Totally arbitrary initial size - rbuf.writeField("len", 0) - this.setRustBuffer(rbuf) - } - - internal fun setRustBuffer(rbuf: RustBuffer.ByValue) { - this.rbuf = rbuf - this.bbuf = this.rbuf.data?.getByteBuffer(0, this.rbuf.capacity.toLong())?.also { - it.order(ByteOrder.BIG_ENDIAN) - it.position(rbuf.len) - } - } - - fun finalize() : RustBuffer.ByValue { - val rbuf = this.rbuf - // Ensure that the JVM-level field is written through to native memory - // before turning the buffer, in case its recipient uses it in a context - // JNA doesn't apply its automatic synchronization logic. - rbuf.writeField("len", this.bbuf!!.position()) - this.setRustBuffer(RustBuffer.ByValue()) - return rbuf - } - - fun discard() { - val rbuf = this.finalize() - RustBuffer.free(rbuf) - } - - internal fun reserve(size: Int, write: (ByteBuffer) -> Unit) { - // TODO: this will perform two checks to ensure we're not overflowing the buffer: - // one here where we check if it needs to grow, and another when we call a write - // method on the ByteBuffer. It might be cheaper to use exception-driven control-flow - // here, trying the write and growing if it throws a `BufferOverflowException`. - // Benchmarking needed. - if (this.bbuf!!.position() + size > this.rbuf.capacity) { - rbuf.writeField("len", this.bbuf!!.position()) - this.setRustBuffer(RustBuffer.reserve(this.rbuf, size)) - } - write(this.bbuf!!) - } - - fun putByte(v: Byte) { - this.reserve(1) { bbuf -> - bbuf.put(v) - } - } - - fun putShort(v: Short) { - this.reserve(2) { bbuf -> - bbuf.putShort(v) - } - } - - fun putInt(v: Int) { - this.reserve(4) { bbuf -> - bbuf.putInt(v) - } - } - - fun putLong(v: Long) { - this.reserve(8) { bbuf -> - bbuf.putLong(v) - } - } - - fun putFloat(v: Float) { - this.reserve(4) { bbuf -> - bbuf.putFloat(v) - } - } - - fun putDouble(v: Double) { - this.reserve(8) { bbuf -> - bbuf.putDouble(v) - } - } - - fun put(v: ByteArray) { - this.reserve(v.size) { bbuf -> - bbuf.put(v) - } - } -} - -// Helpers for reading primitive data types from a bytebuffer. - -internal fun liftFromRustBuffer(rbuf: RustBuffer.ByValue, readItem: (ByteBuffer) -> T): T { - val buf = rbuf.asByteBuffer()!! - try { - val item = readItem(buf) - if (buf.hasRemaining()) { - throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") - } - return item - } finally { - RustBuffer.free(rbuf) - } -} - -internal fun lowerIntoRustBuffer(v: T, writeItem: (T, RustBufferBuilder) -> Unit): RustBuffer.ByValue { - // TODO: maybe we can calculate some sort of initial size hint? - val buf = RustBufferBuilder() - try { - writeItem(v, buf) - return buf.finalize() - } catch (e: Throwable) { - buf.discard() - throw e - } -} - -// For every type used in the interface, we provide helper methods for conveniently -// lifting and lowering that type from C-compatible data, and for reading and writing -// values of that type in a buffer. - - - - -internal fun String.Companion.lift(rbuf: RustBuffer.ByValue): String { - try { - val byteArr = ByteArray(rbuf.len) - rbuf.asByteBuffer()!!.get(byteArr) - return byteArr.toString(Charsets.UTF_8) - } finally { - RustBuffer.free(rbuf) - } -} - -internal fun String.Companion.read(buf: ByteBuffer): String { - val len = buf.getInt() - val byteArr = ByteArray(len) - buf.get(byteArr) - return byteArr.toString(Charsets.UTF_8) -} - -internal fun String.lower(): RustBuffer.ByValue { - val byteArr = this.toByteArray(Charsets.UTF_8) - // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us - // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. - val rbuf = RustBuffer.alloc(byteArr.size) - rbuf.asByteBuffer()!!.put(byteArr) - return rbuf -} - -internal fun String.write(buf: RustBufferBuilder) { - val byteArr = this.toByteArray(Charsets.UTF_8) - buf.putInt(byteArr.size) - buf.put(byteArr) -} - - - - - - - - - - -@Synchronized -fun findLibraryName(componentName: String): String { - val libOverride = System.getProperty("uniffi.component.${componentName}.libraryOverride") - if (libOverride != null) { - return libOverride - } - return "uniffi_bdk" -} - -inline fun loadIndirect( - componentName: String -): Lib { - return Native.load(findLibraryName(componentName), Lib::class.java) -} - -// A JNA Library to expose the extern-C FFI definitions. -// This is an implementation detail which will be called internally by the public API. - -internal interface _UniFFILib : Library { - companion object { - internal val INSTANCE: _UniFFILib by lazy { - loadIndirect<_UniFFILib>(componentName = "bdk") - - - } - } - - fun ffi_bdk_ed55_OfflineWallet_object_free(ptr: Pointer, - uniffi_out_err: RustCallStatus - ): Unit - - fun bdk_ed55_OfflineWallet_new(descriptor: RustBuffer.ByValue, - uniffi_out_err: RustCallStatus - ): Pointer - - fun ffi_bdk_ed55_rustbuffer_alloc(size: Int, - uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - - fun ffi_bdk_ed55_rustbuffer_from_bytes(bytes: ForeignBytes.ByValue, - uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - - fun ffi_bdk_ed55_rustbuffer_free(buf: RustBuffer.ByValue, - uniffi_out_err: RustCallStatus - ): Unit - - fun ffi_bdk_ed55_rustbuffer_reserve(buf: RustBuffer.ByValue,additional: Int, - uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - - -} - -// A handful of classes and functions to support the generated data structures. -// This would be a good candidate for isolating in its own ffi-support lib. - - - -// Interface implemented by anything that can contain an object reference. -// -// Such types expose a `destroy()` method that must be called to cleanly -// dispose of the contained objects. Failure to call this method may result -// in memory leaks. -// -// The easiest way to ensure this method is called is to use the `.use` -// helper method to execute a block and destroy the object at the end. -interface Disposable { - fun destroy() -} - -inline fun T.use(block: (T) -> R) = - try { - block(this) - } finally { - try { - // N.B. our implementation is on the nullable type `Disposable?`. - this?.destroy() - } catch (e: Throwable) { - // swallow - } - } - -// The base class for all UniFFI Object types. -// -// This class provides core operations for working with the Rust `Arc` pointer to -// the live Rust struct on the other side of the FFI. -// -// There's some subtlety here, because we have to be careful not to operate on a Rust -// struct after it has been dropped, and because we must expose a public API for freeing -// the Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: -// -// * Each `FFIObject` instance holds an opaque pointer to the underlying Rust struct. -// Method calls need to read this pointer from the object's state and pass it in to -// the Rust FFI. -// -// * When an `FFIObject` is no longer needed, its pointer should be passed to a -// special destructor function provided by the Rust FFI, which will drop the -// underlying Rust struct. -// -// * Given an `FFIObject` instance, calling code is expected to call the special -// `destroy` method in order to free it after use, either by calling it explicitly -// or by using a higher-level helper like the `use` method. Failing to do so will -// leak the underlying Rust struct. -// -// * We can't assume that calling code will do the right thing, and must be prepared -// to handle Kotlin method calls executing concurrently with or even after a call to -// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. -// -// * We must never allow Rust code to operate on the underlying Rust struct after -// the destructor has been called, and must never call the destructor more than once. -// Doing so may trigger memory unsafety. -// -// If we try to implement this with mutual exclusion on access to the pointer, there is the -// possibility of a race between a method call and a concurrent call to `destroy`: -// -// * Thread A starts a method call, reads the value of the pointer, but is interrupted -// before it can pass the pointer over the FFI to Rust. -// * Thread B calls `destroy` and frees the underlying Rust struct. -// * Thread A resumes, passing the already-read pointer value to Rust and triggering -// a use-after-free. -// -// One possible solution would be to use a `ReadWriteLock`, with each method call taking -// a read lock (and thus allowed to run concurrently) and the special `destroy` method -// taking a write lock (and thus blocking on live method calls). However, we aim not to -// generate methods with any hidden blocking semantics, and a `destroy` method that might -// block if called incorrectly seems to meet that bar. -// -// So, we achieve our goals by giving each `FFIObject` an associated `AtomicLong` counter to track -// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` -// has been called. These are updated according to the following rules: -// -// * The initial value of the counter is 1, indicating a live object with no in-flight calls. -// The initial value for the flag is false. -// -// * At the start of each method call, we atomically check the counter. -// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. -// If it is nonzero them we atomically increment it by 1 and proceed with the method call. -// -// * At the end of each method call, we atomically decrement and check the counter. -// If it has reached zero then we destroy the underlying Rust struct. -// -// * When `destroy` is called, we atomically flip the flag from false to true. -// If the flag was already true we silently fail. -// Otherwise we atomically decrement and check the counter. -// If it has reached zero then we destroy the underlying Rust struct. -// -// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, -// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. -// -// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been -// called *and* all in-flight method calls have completed, avoiding violating any of the expectations -// of the underlying Rust code. -// -// In the future we may be able to replace some of this with automatic finalization logic, such as using -// the new "Cleaner" functionaility in Java 9. The above scheme has been designed to work even if `destroy` is -// invoked by garbage-collection machinery rather than by calling code (which by the way, it's apparently also -// possible for the JVM to finalize an object while there is an in-flight call to one of its methods [1], -// so there would still be some complexity here). -// -// Sigh...all of this for want of a robust finalization mechanism. -// -// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 -// -abstract class FFIObject( - protected val pointer: Pointer -): Disposable, AutoCloseable { - - val wasDestroyed = AtomicBoolean(false) - val callCounter = AtomicLong(1) - - open protected fun freeRustArcPtr() { - // To be overridden in subclasses. - } - - override fun destroy() { - // Only allow a single call to this method. - // TODO: maybe we should log a warning if called more than once? - if (this.wasDestroyed.compareAndSet(false, true)) { - // This decrement always matches the initial count of 1 given at creation time. - if (this.callCounter.decrementAndGet() == 0L) { - this.freeRustArcPtr() - } - } - } - - @Synchronized - override fun close() { - this.destroy() - } - - internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { - // Check and increment the call counter, to keep the object alive. - // This needs a compare-and-set retry loop in case of concurrent updates. - do { - val c = this.callCounter.get() - if (c == 0L) { - throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") - } - if (c == Long.MAX_VALUE) { - throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") - } - } while (! this.callCounter.compareAndSet(c, c + 1L)) - // Now we can safely do the method call without the pointer being freed concurrently. - try { - return block(this.pointer) - } finally { - // This decrement aways matches the increment we performed above. - if (this.callCounter.decrementAndGet() == 0L) { - this.freeRustArcPtr() - } - } - } -} - - - - - -// Public interface members begin here. -// Public facing enums -// Error definitions -@Structure.FieldOrder("code", "error_buf") -internal open class RustCallStatus : Structure() { - @JvmField var code: Int = 0 - @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() - - fun isSuccess(): Boolean { - return code == 0 - } - - fun isError(): Boolean { - return code == 1 - } - - fun isPanic(): Boolean { - return code == 2 - } -} - -class InternalException(message: String) : Exception(message) - -// Each top-level error class has a companion object that can lift the error from the call status's rust buffer -interface CallStatusErrorHandler { - fun lift(error_buf: RustBuffer.ByValue): E; -} - -// Helpers for calling Rust -// In practice we usually need to be synchronized to call this safely, so it doesn't -// synchronize itself - -// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err -private inline fun rustCallWithError(errorHandler: CallStatusErrorHandler, callback: (RustCallStatus) -> U): U { - var status = RustCallStatus(); - val return_value = callback(status) - if (status.isSuccess()) { - return return_value - } else if (status.isError()) { - throw errorHandler.lift(status.error_buf) - } else if (status.isPanic()) { - // when the rust code sees a panic, it tries to construct a rustbuffer - // with the message. but if that code panics, then it just sends back - // an empty buffer. - if (status.error_buf.len > 0) { - throw InternalException(String.lift(status.error_buf)) - } else { - throw InternalException("Rust panic") - } - } else { - throw InternalException("Unknown rust call status: $status.code") - } -} - -// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR -object NullCallStatusErrorHandler: CallStatusErrorHandler { - override fun lift(error_buf: RustBuffer.ByValue): InternalException { - RustBuffer.free(error_buf) - return InternalException("Unexpected CALL_ERROR") - } -} - -// Call a rust function that returns a plain value -private inline fun rustCall(callback: (RustCallStatus) -> U): U { - return rustCallWithError(NullCallStatusErrorHandler, callback); -} - -// Public facing records - -// Namespace functions - - -// Objects - - -public interface OfflineWalletInterface { - -} - - -class OfflineWallet( - pointer: Pointer -) : FFIObject(pointer), OfflineWalletInterface { - constructor(descriptor: String ) : - this( - rustCall() { status -> - _UniFFILib.INSTANCE.bdk_ed55_OfflineWallet_new(descriptor.lower() ,status) -}) - - /** - * Disconnect the object from the underlying Rust object. - * - * It can be called more than once, but once called, interacting with the object - * causes an `IllegalStateException`. - * - * Clients **must** call this method once done with the object, or cause a memory leak. - */ - override protected fun freeRustArcPtr() { - rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_ed55_OfflineWallet_object_free(this.pointer, status) - } - } - - internal fun lower(): Pointer = callWithPointer { it } - - internal fun write(buf: RustBufferBuilder) { - // The Rust code always expects pointers written as 8 bytes, - // and will fail to compile if they don't fit. - buf.putLong(Pointer.nativeValue(this.lower())) - } - - - - companion object { - internal fun lift(ptr: Pointer): OfflineWallet { - return OfflineWallet(ptr) - } - - internal fun read(buf: ByteBuffer): OfflineWallet { - // The Rust code always writes pointers as 8 bytes, and will - // fail to compile if they don't fit. - return OfflineWallet.lift(Pointer(buf.getLong())) - } - - - } -} - - -// Callback Interfaces - - diff --git a/src/wallet/blockchain.rs b/src/wallet/blockchain.rs deleted file mode 100644 index 50742f8..0000000 --- a/src/wallet/blockchain.rs +++ /dev/null @@ -1,106 +0,0 @@ -use ::safer_ffi::prelude::*; -use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig}; -use safer_ffi::boxed::Box; -use safer_ffi::char_p::char_p_ref; - -#[derive_ReprC] -#[ReprC::opaque] -#[derive(Debug)] -pub struct BlockchainConfig { - pub raw: AnyBlockchainConfig, -} - -#[ffi_export] -fn new_electrum_config( - url: char_p_ref, - socks5: Option, - retry: i16, - timeout: i16, - stop_gap: usize, -) -> Box { - let url = url.to_string(); - let socks5 = socks5.map(|s| s.to_string()); - let retry = short_to_u8(retry); - let timeout = short_to_optional_u8(timeout); - - let electrum_config = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { - url, - socks5, - retry, - timeout, - stop_gap, - }); - Box::new(BlockchainConfig { - raw: electrum_config, - }) -} - -#[ffi_export] -fn free_blockchain_config(blockchain_config: Box) { - drop(blockchain_config); -} - -// TODO compact_filter rocksdb not compiling on android, switch to sqlite? -//#[derive_ReprC] -//#[repr(C)] -//#[derive(Debug)] -//pub struct BitcoinPeerConfig { -// pub address: char_p_boxed, -// pub socks5: Option, -// pub socks5_credentials: Option>>, -//} -// -//impl From<&BitcoinPeerConfig> for BdkBitcoinPeerConfig { -// fn from(config: &BitcoinPeerConfig) -> Self { -// let address = config.address.to_string(); -// let socks5 = config.socks5.as_ref().map(|p| p.to_string()); -// let socks5_credentials = config -// .socks5_credentials.as_ref() -// .map(|c| (c._0.to_string(), c._1.to_string())); -// -// BdkBitcoinPeerConfig { -// address, -// socks5: socks5, -// socks5_credentials: socks5_credentials, -// } -// } -//} -// -// -//#[ffi_export] -//fn new_compact_filters_config<'lt>( -// peers: c_slice::Ref<'lt, BitcoinPeerConfig>, -// network: char_p_ref, -// storage_dir: char_p_ref, -// skip_blocks: usize, -//) -> Box { -// let peers = peers.iter().map(|p| p.into()).collect(); -// let network = Network::from_str(network.to_str()).unwrap(); -// let storage_dir = storage_dir.to_string(); -// let skip_blocks = Some(skip_blocks); -// let cf_config = AnyBlockchainConfig::CompactFilters(CompactFiltersBlockchainConfig { -// peers, -// network, -// storage_dir, -// skip_blocks, -// }); -// Box::new(BlockchainConfig { raw: cf_config }) -//} - -// utility functions - -fn short_to_optional_u8(short: i16) -> Option { - if short < 0 { - None - } else { - Some(short_to_u8(short)) - } -} - -fn short_to_u8(short: i16) -> u8 { - if short < 0 { - u8::MIN - } else { - u8::try_from(short).unwrap_or(u8::MAX) - } -} diff --git a/src/wallet/database.rs b/src/wallet/database.rs deleted file mode 100644 index 76dc333..0000000 --- a/src/wallet/database.rs +++ /dev/null @@ -1,32 +0,0 @@ -use ::safer_ffi::prelude::*; -use bdk::database::any::SledDbConfiguration; -use bdk::database::AnyDatabaseConfig; -use safer_ffi::boxed::Box; -use safer_ffi::char_p::char_p_ref; - -#[derive_ReprC] -#[ReprC::opaque] -#[derive(Debug)] -pub struct DatabaseConfig { - pub raw: AnyDatabaseConfig, -} - -#[ffi_export] -fn new_memory_config() -> Box { - let memory_config = AnyDatabaseConfig::Memory(()); - Box::new(DatabaseConfig { raw: memory_config }) -} - -#[ffi_export] -fn new_sled_config(path: char_p_ref, tree_name: char_p_ref) -> Box { - let path = path.to_string(); - let tree_name = tree_name.to_string(); - - let sled_config = AnyDatabaseConfig::Sled(SledDbConfiguration { path, tree_name }); - Box::new(DatabaseConfig { raw: sled_config }) -} - -#[ffi_export] -fn free_database_config(database_config: Box) { - drop(database_config); -} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs deleted file mode 100644 index 4637efc..0000000 --- a/src/wallet/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::convert::TryFrom; -use std::ffi::CString; - -use ::safer_ffi::prelude::*; -use bdk::blockchain::{log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain}; -use bdk::database::{AnyDatabase, AnyDatabaseConfig, ConfigurableDatabase}; -use bdk::wallet::AddressIndex::New; -use bdk::{Error, Wallet}; -use safer_ffi::boxed::Box; -use safer_ffi::char_p::{char_p_boxed, char_p_ref}; - -use blockchain::BlockchainConfig; -use database::DatabaseConfig; - -use crate::error::FfiError; -use crate::types::{FfiResult, FfiResultVoid}; -use crate::wallet::transaction::{LocalUtxo, TransactionDetails}; -use bdk::bitcoin::Network; -use std::str::FromStr; - -mod blockchain; -mod database; -mod transaction; - -// create a new wallet - -#[derive_ReprC] -#[ReprC::opaque] -pub struct OpaqueWallet { - raw: Wallet, -} - -#[ffi_export] -fn new_wallet_result( - descriptor: char_p_ref, - change_descriptor: Option, - network: char_p_ref, - blockchain_config: &BlockchainConfig, - database_config: &DatabaseConfig, -) -> FfiResult>> { - let descriptor = descriptor.to_string(); - let change_descriptor = change_descriptor.map(|s| s.to_string()); - let net = Network::from_str(network.to_str()).expect("Network name"); - let bc_config = &blockchain_config.raw; - let db_config = &database_config.raw; - let wallet_result = new_wallet(descriptor, change_descriptor, net, bc_config, db_config); - - match wallet_result { - Ok(w) => FfiResult { - ok: Some(Box::new(OpaqueWallet { raw: w })), - err: FfiError::None, - }, - Err(e) => FfiResult { - ok: None, - err: FfiError::from(&e), - }, - } -} - -fn new_wallet( - descriptor: String, - change_descriptor: Option, - network: Network, - blockchain_config: &AnyBlockchainConfig, - database_config: &AnyDatabaseConfig, -) -> Result, Error> { - let client = AnyBlockchain::from_config(blockchain_config)?; - let database = AnyDatabase::from_config(database_config)?; - - let descriptor: &str = descriptor.as_str(); - let change_descriptor: Option<&str> = change_descriptor.as_deref(); - - Wallet::new(descriptor, change_descriptor, network, database, client) -} - -#[ffi_export] -fn free_wallet_result(wallet_result: FfiResult>>) { - drop(wallet_result); -} - -// wallet operations - -#[ffi_export] -fn sync_wallet(opaque_wallet: &OpaqueWallet) -> FfiResultVoid { - let int_result = opaque_wallet.raw.sync(log_progress(), Some(100)); - match int_result { - Ok(_v) => FfiResultVoid { - err: FfiError::None, - }, - Err(e) => FfiResultVoid { - err: FfiError::from(&e), - }, - } -} - -#[ffi_export] -fn new_address(opaque_wallet: &OpaqueWallet) -> FfiResult { - let new_address = opaque_wallet.raw.get_address(New); - let string_result = new_address.map(|a| a.to_string()); - match string_result { - Ok(a) => FfiResult { - ok: char_p_boxed::try_from(a).unwrap(), - err: FfiError::None, - }, - Err(e) => FfiResult { - ok: char_p_boxed::from(CString::default()), - err: FfiError::from(&e), - }, - } -} - -#[ffi_export] -fn list_unspent(opaque_wallet: &OpaqueWallet) -> FfiResult> { - let unspent_result = opaque_wallet.raw.list_unspent(); - - match unspent_result { - Ok(v) => FfiResult { - ok: { - let ve: Vec = v.iter().map(|lu| LocalUtxo::from(lu)).collect(); - repr_c::Vec::from(ve) - }, - err: FfiError::None, - }, - Err(e) => FfiResult { - ok: repr_c::Vec::EMPTY, - err: FfiError::from(&e), - }, - } -} - -#[ffi_export] -fn free_veclocalutxo_result(unspent_result: FfiResult>) { - drop(unspent_result) -} - -#[ffi_export] -fn balance(opaque_wallet: &OpaqueWallet) -> FfiResult { - let balance_result = opaque_wallet.raw.get_balance(); - - match balance_result { - Ok(b) => FfiResult { - ok: b, - err: FfiError::None, - }, - Err(e) => FfiResult { - ok: u64::MIN, - err: FfiError::from(&e), - }, - } -} - -#[ffi_export] -fn list_transactions(opaque_wallet: &OpaqueWallet) -> FfiResult> { - let transactions_result = opaque_wallet.raw.list_transactions(false); - - match transactions_result { - Ok(v) => FfiResult { - ok: { - let ve: Vec = - v.iter().map(|t| TransactionDetails::from(t)).collect(); - repr_c::Vec::from(ve) - }, - err: FfiError::None, - }, - Err(e) => FfiResult { - ok: repr_c::Vec::EMPTY, - err: FfiError::from(&e), - }, - } -} - -#[ffi_export] -fn free_vectxdetails_result(txdetails_result: FfiResult>) { - drop(txdetails_result) -} diff --git a/src/wallet/transaction.rs b/src/wallet/transaction.rs deleted file mode 100644 index 5c2194c..0000000 --- a/src/wallet/transaction.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::convert::TryFrom; - -use ::safer_ffi::prelude::*; -use safer_ffi::char_p::char_p_boxed; - -// Non-opaque returned values - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct TransactionDetails { - // TODO Optional transaction - // pub transaction: Option, - /// Transaction id - pub txid: char_p_boxed, - /// Received value (sats) - pub received: u64, - /// Sent value (sats) - pub sent: u64, - /// Fee value (sats) if known, -1 if unknown, based on backend - pub fee: i64, - /// true if confirmed - pub is_confirmed: bool, - /// Confirmed in block height - pub confirmation_time: ConfirmationTime, - /// Whether the tx has been verified against the consensus rules - pub verified: bool, -} - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct ConfirmationTime { - /// confirmation block height, 0 if is_confirmed is false - pub height: u32, - /// confirmation block timestamp, 0 if is_confirmed is false - pub timestamp: u64, -} - -impl From<&bdk::TransactionDetails> for TransactionDetails { - fn from(op: &bdk::TransactionDetails) -> Self { - let fee = op.fee.map(|f| i64::try_from(f).unwrap()).unwrap_or(-1); - let confirmation_time = op - .confirmation_time - .as_ref() - .map(|c| ConfirmationTime { - height: c.height, - timestamp: c.timestamp, - }) - .unwrap_or(ConfirmationTime { - height: 0, - timestamp: 0, - }); - TransactionDetails { - txid: char_p_boxed::try_from(op.txid.to_string()).unwrap(), - received: op.received, - sent: op.sent, - fee, - is_confirmed: op.confirmation_time.is_some(), - confirmation_time, - verified: op.verified, - } - } -} - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct OutPoint { - /// The referenced transaction's txid, as hex string - pub txid: char_p_boxed, - /// The index of the referenced output in its transaction's vout - pub vout: u32, -} - -impl From<&bdk::bitcoin::OutPoint> for OutPoint { - fn from(op: &bdk::bitcoin::OutPoint) -> Self { - OutPoint { - txid: char_p_boxed::try_from(op.txid.to_string()).unwrap(), - vout: op.vout, - } - } -} - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct TxOut { - /// The value of the output, in satoshis - pub value: u64, - /// The script which must satisfy for the output to be spent, as hex string - pub script_pubkey: char_p_boxed, -} - -impl From<&bdk::bitcoin::TxOut> for TxOut { - fn from(to: &bdk::bitcoin::TxOut) -> Self { - TxOut { - value: to.value, - script_pubkey: char_p_boxed::try_from(to.script_pubkey.to_string()).unwrap(), - } - } -} - -#[derive_ReprC] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct LocalUtxo { - /// Reference to a transaction output - pub outpoint: OutPoint, - /// Transaction output - pub txout: TxOut, - /// Type of keychain, as short 0 for "external" or 1 for "internal" - pub keychain: u16, -} - -impl From<&bdk::LocalUtxo> for LocalUtxo { - fn from(lu: &bdk::LocalUtxo) -> Self { - LocalUtxo { - outpoint: OutPoint::from(&lu.outpoint), - txout: TxOut::from(&lu.txout), - keychain: lu.keychain as u16, - } - } -}