diff --git a/Cargo.toml b/Cargo.toml index d59366f..954e596 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -bdk = { version = "^0.12.1-dev", features = ["all-keys"] } +bdk = { version = "^0.12.1-dev", features = ["all-keys", "use-esplora-ureq"] } uniffi_macros = "0.14.0" uniffi = "0.14.0" thiserror = "1.0" diff --git a/bindings/bdk-kotlin/build.gradle b/bindings/bdk-kotlin/build.gradle index bb1b0c0..9da0de2 100644 --- a/bindings/bdk-kotlin/build.gradle +++ b/bindings/bdk-kotlin/build.gradle @@ -20,6 +20,13 @@ allprojects { google() mavenCentral() } + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs += [ + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + ] + } + } } dependencies { @@ -32,4 +39,4 @@ dependencies { java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 -} \ No newline at end of file +} diff --git a/bindings/bdk-kotlin/src/main/kotlin/uniffi/bdk/bdk.kt b/bindings/bdk-kotlin/src/main/kotlin/uniffi/bdk/bdk.kt index 6343671..4d7121a 100644 --- a/bindings/bdk-kotlin/src/main/kotlin/uniffi/bdk/bdk.kt +++ b/bindings/bdk-kotlin/src/main/kotlin/uniffi/bdk/bdk.kt @@ -44,15 +44,15 @@ open class RustBuffer : Structure() { companion object { internal fun alloc(size: Int = 0) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_146a_rustbuffer_alloc(size, status) + _UniFFILib.INSTANCE.ffi_bdk_f2ea_rustbuffer_alloc(size, status) } internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_146a_rustbuffer_free(buf, status) + _UniFFILib.INSTANCE.ffi_bdk_f2ea_rustbuffer_free(buf, status) } internal fun reserve(buf: RustBuffer.ByValue, additional: Int) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_146a_rustbuffer_reserve(buf, additional, status) + _UniFFILib.INSTANCE.ffi_bdk_f2ea_rustbuffer_reserve(buf, additional, status) } } @@ -208,6 +208,54 @@ internal fun lowerIntoRustBuffer(v: T, writeItem: (T, RustBufferBuilder) -> U +@ExperimentalUnsignedTypes +internal fun UByte.Companion.lift(v: Byte): UByte { + return v.toUByte() +} + +@ExperimentalUnsignedTypes +internal fun UByte.Companion.read(buf: ByteBuffer): UByte { + return UByte.lift(buf.get()) +} + +@ExperimentalUnsignedTypes +internal fun UByte.lower(): Byte { + return this.toByte() +} + +@ExperimentalUnsignedTypes +internal fun UByte.write(buf: RustBufferBuilder) { + buf.putByte(this.toByte()) +} + + + + + +@ExperimentalUnsignedTypes +internal fun ULong.Companion.lift(v: Long): ULong { + return v.toULong() +} + +@ExperimentalUnsignedTypes +internal fun ULong.Companion.read(buf: ByteBuffer): ULong { + return ULong.lift(buf.getLong()) +} + +@ExperimentalUnsignedTypes +internal fun ULong.lower(): Long { + return this.toLong() +} + +@ExperimentalUnsignedTypes +internal fun ULong.write(buf: RustBufferBuilder) { + buf.putLong(this.toLong()) +} + + + + + internal fun String.Companion.lift(rbuf: RustBuffer.ByValue): String { try { val byteArr = ByteArray(rbuf.len) @@ -274,6 +322,108 @@ internal fun String.write(buf: RustBufferBuilder) { + + + + + + + + + + + + + + + + + + + + + + + + + + + +// Helper functions for pasing values of type UByte? +@ExperimentalUnsignedTypes +internal fun liftOptionalu8(rbuf: RustBuffer.ByValue): UByte? { + return liftFromRustBuffer(rbuf) { buf -> + readOptionalu8(buf) + } +} + +@ExperimentalUnsignedTypes +internal fun readOptionalu8(buf: ByteBuffer): UByte? { + if (buf.get().toInt() == 0) { + return null + } + return UByte.read(buf) +} + +@ExperimentalUnsignedTypes +internal fun lowerOptionalu8(v: UByte?): RustBuffer.ByValue { + return lowerIntoRustBuffer(v) { v, buf -> + writeOptionalu8(v, buf) + } +} + +@ExperimentalUnsignedTypes +internal fun writeOptionalu8(v: UByte?, buf: RustBufferBuilder) { + if (v == null) { + buf.putByte(0) + } else { + buf.putByte(1) + v.write(buf) + } +} + + + + + + + +// Helper functions for pasing values of type String? + +internal fun liftOptionalstring(rbuf: RustBuffer.ByValue): String? { + return liftFromRustBuffer(rbuf) { buf -> + readOptionalstring(buf) + } +} + + +internal fun readOptionalstring(buf: ByteBuffer): String? { + if (buf.get().toInt() == 0) { + return null + } + return String.read(buf) +} + + +internal fun lowerOptionalstring(v: String?): RustBuffer.ByValue { + return lowerIntoRustBuffer(v) { v, buf -> + writeOptionalstring(v, buf) + } +} + + +internal fun writeOptionalstring(v: String?, buf: RustBufferBuilder) { + if (v == null) { + buf.putByte(0) + } else { + buf.putByte(1) + v.write(buf) + } +} + + + + @Synchronized fun findLibraryName(componentName: String): String { val libOverride = System.getProperty("uniffi.component.${componentName}.libraryOverride") @@ -301,31 +451,39 @@ internal interface _UniFFILib : Library { } } - fun ffi_bdk_146a_OfflineWallet_object_free(ptr: Pointer, + fun ffi_bdk_f2ea_OfflineWallet_object_free(ptr: Pointer, uniffi_out_err: RustCallStatus ): Unit - fun bdk_146a_OfflineWallet_new(descriptor: RustBuffer.ByValue,network: RustBuffer.ByValue,database_config: RustBuffer.ByValue, + fun bdk_f2ea_OfflineWallet_new(descriptor: RustBuffer.ByValue,network: RustBuffer.ByValue,database_config: RustBuffer.ByValue, uniffi_out_err: RustCallStatus ): Pointer - fun bdk_146a_OfflineWallet_get_new_address(ptr: Pointer, + fun bdk_f2ea_OfflineWallet_get_new_address(ptr: Pointer, uniffi_out_err: RustCallStatus ): RustBuffer.ByValue - fun ffi_bdk_146a_rustbuffer_alloc(size: Int, - uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - - fun ffi_bdk_146a_rustbuffer_from_bytes(bytes: ForeignBytes.ByValue, - uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - - fun ffi_bdk_146a_rustbuffer_free(buf: RustBuffer.ByValue, + fun ffi_bdk_f2ea_OnlineWallet_object_free(ptr: Pointer, uniffi_out_err: RustCallStatus ): Unit - fun ffi_bdk_146a_rustbuffer_reserve(buf: RustBuffer.ByValue,additional: Int, + fun bdk_f2ea_OnlineWallet_new(descriptor: RustBuffer.ByValue,network: RustBuffer.ByValue,database_config: RustBuffer.ByValue,blockchain_config: RustBuffer.ByValue, + uniffi_out_err: RustCallStatus + ): Pointer + + fun ffi_bdk_f2ea_rustbuffer_alloc(size: Int, + uniffi_out_err: RustCallStatus + ): RustBuffer.ByValue + + fun ffi_bdk_f2ea_rustbuffer_from_bytes(bytes: ForeignBytes.ByValue, + uniffi_out_err: RustCallStatus + ): RustBuffer.ByValue + + fun ffi_bdk_f2ea_rustbuffer_free(buf: RustBuffer.ByValue, + uniffi_out_err: RustCallStatus + ): Unit + + fun ffi_bdk_f2ea_rustbuffer_reserve(buf: RustBuffer.ByValue,additional: Int, uniffi_out_err: RustCallStatus ): RustBuffer.ByValue @@ -543,7 +701,7 @@ sealed class DatabaseConfig { ) : DatabaseConfig() data class Sled( - val configuration: SledDbConfiguration + val config: SledDbConfiguration ) : DatabaseConfig() @@ -578,7 +736,67 @@ sealed class DatabaseConfig { } is DatabaseConfig.Sled -> { buf.putInt(2) - this.configuration.write(buf) + this.config.write(buf) + + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + + + +} + + + + + + + + +@ExperimentalUnsignedTypes +sealed class BlockchainConfig { + + data class Electrum( + val config: ElectrumConfig + ) : BlockchainConfig() + + data class Esplora( + val config: EsploraConfig + ) : BlockchainConfig() + + + companion object { + internal fun lift(rbuf: RustBuffer.ByValue): BlockchainConfig { + return liftFromRustBuffer(rbuf) { buf -> BlockchainConfig.read(buf) } + } + + internal fun read(buf: ByteBuffer): BlockchainConfig { + return when(buf.getInt()) { + 1 -> BlockchainConfig.Electrum( + ElectrumConfig.read(buf) + ) + 2 -> BlockchainConfig.Esplora( + EsploraConfig.read(buf) + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + } + + internal fun lower(): RustBuffer.ByValue { + return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)}) + } + + internal fun write(buf: RustBufferBuilder) { + when(this) { + is BlockchainConfig.Electrum -> { + buf.putInt(1) + this.config.write(buf) + + } + is BlockchainConfig.Esplora -> { + buf.putInt(2) + this.config.write(buf) } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } @@ -656,6 +874,7 @@ sealed class BdkException(message: String): Exception(message) { class Psbt(message: String) : BdkException(message) class PsbtParse(message: String) : BdkException(message) class Electrum(message: String) : BdkException(message) + class Esplora(message: String) : BdkException(message) class Sled(message: String) : BdkException(message) @@ -704,7 +923,8 @@ sealed class BdkException(message: String): Exception(message) { 35 -> BdkException.Psbt(String.read(error_buf)) 36 -> BdkException.PsbtParse(String.read(error_buf)) 37 -> BdkException.Electrum(String.read(error_buf)) - 38 -> BdkException.Sled(String.read(error_buf)) + 38 -> BdkException.Esplora(String.read(error_buf)) + 39 -> BdkException.Sled(String.read(error_buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -786,6 +1006,96 @@ data class SledDbConfiguration ( +} + +@ExperimentalUnsignedTypes +data class ElectrumConfig ( + var url: String, + var socks5: String?, + var retry: UByte, + var timeout: UByte?, + var stopGap: ULong +) { + companion object { + internal fun lift(rbuf: RustBuffer.ByValue): ElectrumConfig { + return liftFromRustBuffer(rbuf) { buf -> ElectrumConfig.read(buf) } + } + + internal fun read(buf: ByteBuffer): ElectrumConfig { + return ElectrumConfig( + String.read(buf), + readOptionalstring(buf), + UByte.read(buf), + readOptionalu8(buf), + ULong.read(buf) + ) + } + } + + internal fun lower(): RustBuffer.ByValue { + return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)}) + } + + internal fun write(buf: RustBufferBuilder) { + this.url.write(buf) + + writeOptionalstring(this.socks5, buf) + + this.retry.write(buf) + + writeOptionalu8(this.timeout, buf) + + this.stopGap.write(buf) + + } + + + +} + +@ExperimentalUnsignedTypes +data class EsploraConfig ( + var baseUrl: String, + var proxy: String?, + var timeoutRead: ULong, + var timeoutWrite: ULong, + var stopGap: ULong +) { + companion object { + internal fun lift(rbuf: RustBuffer.ByValue): EsploraConfig { + return liftFromRustBuffer(rbuf) { buf -> EsploraConfig.read(buf) } + } + + internal fun read(buf: ByteBuffer): EsploraConfig { + return EsploraConfig( + String.read(buf), + readOptionalstring(buf), + ULong.read(buf), + ULong.read(buf), + ULong.read(buf) + ) + } + } + + internal fun lower(): RustBuffer.ByValue { + return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)}) + } + + internal fun write(buf: RustBufferBuilder) { + this.baseUrl.write(buf) + + writeOptionalstring(this.proxy, buf) + + this.timeoutRead.write(buf) + + this.timeoutWrite.write(buf) + + this.stopGap.write(buf) + + } + + + } @@ -807,7 +1117,7 @@ class OfflineWallet( constructor(descriptor: String, network: Network, databaseConfig: DatabaseConfig ) : this( rustCallWithError(BdkException) { status -> - _UniFFILib.INSTANCE.bdk_146a_OfflineWallet_new(descriptor.lower(), network.lower(), databaseConfig.lower() ,status) + _UniFFILib.INSTANCE.bdk_f2ea_OfflineWallet_new(descriptor.lower(), network.lower(), databaseConfig.lower() ,status) }) /** @@ -820,7 +1130,7 @@ class OfflineWallet( */ override protected fun freeRustArcPtr() { rustCall() { status -> - _UniFFILib.INSTANCE.ffi_bdk_146a_OfflineWallet_object_free(this.pointer, status) + _UniFFILib.INSTANCE.ffi_bdk_f2ea_OfflineWallet_object_free(this.pointer, status) } } @@ -835,7 +1145,7 @@ class OfflineWallet( override fun getNewAddress(): String = callWithPointer { rustCall() { status -> - _UniFFILib.INSTANCE.bdk_146a_OfflineWallet_get_new_address(it, status) + _UniFFILib.INSTANCE.bdk_f2ea_OfflineWallet_get_new_address(it, status) } }.let { String.lift(it) @@ -858,6 +1168,60 @@ class OfflineWallet( } } +@ExperimentalUnsignedTypes +public interface OnlineWalletInterface { + +} + +@ExperimentalUnsignedTypes +class OnlineWallet( + pointer: Pointer +) : FFIObject(pointer), OnlineWalletInterface { + constructor(descriptor: String, network: Network, databaseConfig: DatabaseConfig, blockchainConfig: BlockchainConfig ) : + this( + rustCallWithError(BdkException) { status -> + _UniFFILib.INSTANCE.bdk_f2ea_OnlineWallet_new(descriptor.lower(), network.lower(), databaseConfig.lower(), blockchainConfig.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_f2ea_OnlineWallet_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): OnlineWallet { + return OnlineWallet(ptr) + } + + internal fun read(buf: ByteBuffer): OnlineWallet { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return OnlineWallet.lift(Pointer(buf.getLong())) + } + + + } +} + // Callback Interfaces diff --git a/bindings/bdk-kotlin/src/test/kotlin/uniffi/bdk/LibTest.kt b/bindings/bdk-kotlin/src/test/kotlin/uniffi/bdk/LibTest.kt index da33bfb..6423d2b 100644 --- a/bindings/bdk-kotlin/src/test/kotlin/uniffi/bdk/LibTest.kt +++ b/bindings/bdk-kotlin/src/test/kotlin/uniffi/bdk/LibTest.kt @@ -35,4 +35,12 @@ class LibTest { assertNotNull(address) assertEquals(address, "bcrt1qzg4mckdh50nwdm9hkzq06528rsu73hjxytqkxs") } + + @Test + fun onlineWalletInMemory() { + val db = DatabaseConfig.Memory("") + val client = BlockchainConfig.Electrum(ElectrumConfig("ssl://electrum.blockstream.info:50002", null, 5u, null, 100u)) + val wallet = OnlineWallet(desc, Network.TESTNET, db, client) + assertNotNull(wallet) + } } diff --git a/src/bdk.udl b/src/bdk.udl index 2ae60cc..1994bc1 100644 --- a/src/bdk.udl +++ b/src/bdk.udl @@ -41,6 +41,7 @@ enum BdkError { "Psbt", "PsbtParse", "Electrum", + "Esplora", "Sled", }; @@ -59,7 +60,7 @@ dictionary SledDbConfiguration { [Enum] interface DatabaseConfig { Memory(string junk); - Sled(SledDbConfiguration configuration); + Sled(SledDbConfiguration config); }; interface OfflineWallet { @@ -67,3 +68,30 @@ interface OfflineWallet { constructor(string descriptor, Network network, DatabaseConfig database_config); string get_new_address(); }; + +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); +}; + +interface OnlineWallet { + [Throws=BdkError] + constructor(string descriptor, Network network, DatabaseConfig database_config, BlockchainConfig blockchain_config); +}; diff --git a/src/lib.rs b/src/lib.rs index cf0f756..ae6fcc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,14 @@ use bdk::bitcoin::Network; +use bdk::blockchain::any::{AnyBlockchain, AnyBlockchainConfig}; +use bdk::blockchain::{ + electrum::ElectrumBlockchainConfig, esplora::EsploraBlockchainConfig, ConfigurableBlockchain, +}; use bdk::database::any::{AnyDatabase, SledDbConfiguration}; use bdk::database::{AnyDatabaseConfig, ConfigurableDatabase}; use bdk::wallet::AddressIndex; use bdk::Error; use bdk::Wallet; - +use std::convert::TryFrom; use std::sync::Mutex; uniffi_macros::include_scaffolding!("bdk"); @@ -13,7 +17,28 @@ type BdkError = Error; pub enum DatabaseConfig { Memory { junk: String }, - Sled { configuration: SledDbConfiguration }, + Sled { config: SledDbConfiguration }, +} + +pub struct ElectrumConfig { + pub url: String, + pub socks5: Option, + pub retry: u8, + pub timeout: Option, + pub stop_gap: u64, +} + +pub struct EsploraConfig { + pub base_url: String, + pub proxy: Option, + pub timeout_read: u64, + pub timeout_write: u64, + pub stop_gap: u64, +} + +pub enum BlockchainConfig { + Electrum { config: ElectrumConfig }, + Esplora { config: EsploraConfig }, } struct OfflineWallet { @@ -28,7 +53,7 @@ impl OfflineWallet { ) -> Result { let any_database_config = match database_config { DatabaseConfig::Memory { .. } => AnyDatabaseConfig::Memory(()), - DatabaseConfig::Sled { configuration } => AnyDatabaseConfig::Sled(configuration), + 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)?); @@ -46,4 +71,53 @@ impl OfflineWallet { } } +struct OnlineWallet { + wallet: Mutex>, +} + +impl OnlineWallet { + fn new( + descriptor: String, + network: Network, + database_config: DatabaseConfig, + blockchain_config: BlockchainConfig, + ) -> Result { + 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, + None, + network, + database, + blockchain, + )?); + Ok(OnlineWallet { wallet }) + } +} + uniffi::deps::static_assertions::assert_impl_all!(OfflineWallet: Sync, Send); +uniffi::deps::static_assertions::assert_impl_all!(OnlineWallet: Sync, Send);