[WIP] reorganize and remove old stuff

This commit is contained in:
Steve Myers 2021-10-12 15:24:11 -07:00
parent f30558d55c
commit 091c9994fa
11 changed files with 19 additions and 1232 deletions

2
.gitignore vendored
View File

@ -7,4 +7,4 @@ wallet_db
bdk_ffi_test
local.properties
*.log
targets/kotlin/testdb
*.dylib

View File

@ -1,7 +1,7 @@
[package]
name = "uniffi_bdk"
version = "0.1.0"
authors = ["Steve Myers <steve@notmandatory.org>"]
authors = ["Steve Myers <steve@notmandatory.org>", ""]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -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

View File

@ -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)

View File

@ -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<bdk::Error> 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,
}
}
}

View File

@ -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<T> {
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<char_p_boxed>) {
drop(string_result)
}
#[ffi_export]
fn free_void_result(void_result: FfiResultVoid) {
drop(void_result)
}
#[ffi_export]
fn free_uint64_result(void_result: FfiResult<u64>) {
drop(void_result)
}
// TODO do we need this? remove?
/// Free a Rust-allocated string
#[ffi_export]
fn free_string(string: Option<char_p_boxed>) {
drop(string)
}

View File

@ -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<T> 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<T> 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 <reified Lib : Library> loadIndirect(
componentName: String
): Lib {
return Native.load<Lib>(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 : Disposable?, R> 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<T>` 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<T>` 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 <R> 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<E> {
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 <U, E: Exception> rustCallWithError(errorHandler: CallStatusErrorHandler<E>, 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<InternalException> {
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 <U> 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

View File

@ -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<char_p_ref>,
retry: i16,
timeout: i16,
stop_gap: usize,
) -> Box<BlockchainConfig> {
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<BlockchainConfig>) {
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<char_p_boxed>,
// pub socks5_credentials: Option<Box<Tuple2<char_p_boxed, char_p_boxed>>>,
//}
//
//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<BlockchainConfig> {
// 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<u8> {
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)
}
}

View File

@ -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<DatabaseConfig> {
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<DatabaseConfig> {
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<DatabaseConfig>) {
drop(database_config);
}

View File

@ -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<AnyBlockchain, AnyDatabase>,
}
#[ffi_export]
fn new_wallet_result(
descriptor: char_p_ref,
change_descriptor: Option<char_p_ref>,
network: char_p_ref,
blockchain_config: &BlockchainConfig,
database_config: &DatabaseConfig,
) -> FfiResult<Option<Box<OpaqueWallet>>> {
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<String>,
network: Network,
blockchain_config: &AnyBlockchainConfig,
database_config: &AnyDatabaseConfig,
) -> Result<Wallet<AnyBlockchain, AnyDatabase>, 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<Option<Box<OpaqueWallet>>>) {
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<char_p_boxed> {
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<repr_c::Vec<LocalUtxo>> {
let unspent_result = opaque_wallet.raw.list_unspent();
match unspent_result {
Ok(v) => FfiResult {
ok: {
let ve: Vec<LocalUtxo> = 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<repr_c::Vec<LocalUtxo>>) {
drop(unspent_result)
}
#[ffi_export]
fn balance(opaque_wallet: &OpaqueWallet) -> FfiResult<u64> {
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<repr_c::Vec<TransactionDetails>> {
let transactions_result = opaque_wallet.raw.list_transactions(false);
match transactions_result {
Ok(v) => FfiResult {
ok: {
let ve: Vec<TransactionDetails> =
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<repr_c::Vec<TransactionDetails>>) {
drop(txdetails_result)
}

View File

@ -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>,
/// 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,
}
}
}