diff --git a/.gitignore b/.gitignore index 9bf4fb6..fd662a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ target build Cargo.lock -*.h /bdk-kotlin/local.properties .gradle wallet_db diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d3f227 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ + + +Adding new structs and functions + +1. Create C safe Rust structs and related functions using safer-ffi + +2. Test generated library and `bdk_ffi.h` file with c language tests in `cc/bdk_ffi_test.c` + +3. Use `build.sh` and `test.sh` to build c test program and verify functionality and + memory de-allocation via `valgrind` + +4. Update the kotlin native interface LibJna.kt in the `bdk-kotlin` `jvm` module to match `bdk_ffi.h` + +5. Create kotlin wrapper classes and interfaces as needed + +6. Add tests to `bdk-kotlin` `test-fixtures` module + +7. Use `build.sh` and `test.sh` to build and test `bdk-kotlin` `jvm` and `android` modules \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/FfiResult.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/FfiResult.kt new file mode 100644 index 0000000..3755617 --- /dev/null +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/FfiResult.kt @@ -0,0 +1,15 @@ +package org.bitcoindevkit.bdk + +import com.sun.jna.Pointer +import com.sun.jna.Structure + +abstract class FfiResult : Structure { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + + @JvmField + var ok: Pointer? = null + + @JvmField + var err: Pointer? = null +} \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/LibJna.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/LibJna.kt index ca50bb7..bfb5383 100644 --- a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/LibJna.kt +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/LibJna.kt @@ -1,52 +1,48 @@ package org.bitcoindevkit.bdk -import com.sun.jna.Library -import com.sun.jna.Pointer -import com.sun.jna.PointerType +import com.sun.jna.* interface LibJna : Library { - // typedef struct VoidResult VoidResult_t; - class VoidResult_t : PointerType { - constructor() : super() - constructor(pointer: Pointer) : super(pointer) + // typedef struct { + // + // char * * ok; + // + // char * * err; + // + // } FfiResult_char_ptr_t; + open class FfiResult_char_ptr_t : FfiResult() { + class ByValue : FfiResult_char_ptr_t(), Structure.ByValue + class ByReference : FfiResult_char_ptr_t(), Structure.ByReference + + override fun getFieldOrder() = listOf("ok", "err") } - // char * get_void_err ( - // VoidResult_t const * void_result); - fun get_void_err(void_result: VoidResult_t): Pointer? - - // void free_void_result ( - // VoidResult_t * void_result); - fun free_void_result(void_result: VoidResult_t) - - // typedef struct StringResult StringResult_t; - class StringResult_t : PointerType { - constructor() : super() - constructor(pointer: Pointer) : super(pointer) - } - - // char * get_string_ok ( - // StringResult_t const * string_result); - fun get_string_ok(string_result: StringResult_t): Pointer? - - // char * get_string_err ( - // StringResult_t const * string_result); - fun get_string_err(string_result: StringResult_t): Pointer? - // void free_string_result ( - // StringResult_t * string_result); - fun free_string_result(string_result: StringResult_t) + // FfiResult_char_ptr_t string_result); + fun free_string_result(string_result: FfiResult_char_ptr_t.ByValue) - // typedef struct WalletRef WalletRef_t; - class WalletRef_t : PointerType { - constructor() : super() - constructor(pointer: Pointer) : super(pointer) + // typedef struct { + // + // void * ok; + // + // char * * err; + // + // } FfiResult_void_t; + open class FfiResult_void_t : FfiResult() { + class ByValue : FfiResult_void_t(), Structure.ByValue + class ByReference : FfiResult_void_t(), Structure.ByReference + + override fun getFieldOrder() = listOf("ok", "err") } - // void free_wallet_ref ( - // WalletRef_t * wallet_ref); - fun free_wallet_ref(wallet_ref: WalletRef_t) + // void free_void_result ( + // FfiResult_void_t void_result); + fun free_void_result(void_result: FfiResult_void_t.ByValue) + + // void free_string ( + // char * string); + fun free_string(string: Pointer?) // typedef struct BlockchainConfig BlockchainConfig_t; class BlockchainConfig_t : PointerType { @@ -54,54 +50,6 @@ interface LibJna : Library { constructor(pointer: Pointer) : super(pointer) } - // typedef struct DatabaseConfig DatabaseConfig_t; - class DatabaseConfig_t : PointerType { - constructor() : super() - constructor(pointer: Pointer) : super(pointer) - } - - // typedef struct WalletResult WalletResult_t; - class WalletResult_t : PointerType { - constructor() : super() - constructor(pointer: Pointer) : super(pointer) - } - - // WalletResult_t * new_wallet_result ( - // char const * descriptor, - // char const * change_descriptor, - // BlockchainConfig_t const * blockchain_config, - // DatabaseConfig_t const * database_config); - fun new_wallet_result( - descriptor: String, - changeDescriptor: String?, - blockchainConfig: BlockchainConfig_t, - databaseConfig: DatabaseConfig_t, - ): WalletResult_t - - // char * get_wallet_err ( - // WalletResult_t const * wallet_result); - fun get_wallet_err(wallet_result: WalletResult_t): Pointer? - - // WalletRef_t * get_wallet_ok ( - // WalletResult_t const * wallet_result); - fun get_wallet_ok(wallet_result: WalletResult_t): WalletRef_t? - - // VoidResult_t * sync_wallet ( - // WalletRef_t const * wallet_ref); - fun sync_wallet(wallet_ref: Pointer): VoidResult_t - - // StringResult_t * new_address ( - // WalletRef_t const * wallet_ref); - fun new_address(wallet_ref: Pointer): StringResult_t - - // void free_wallet_result ( - // WalletResult_t * wallet_result); - fun free_wallet_result(wallet_result: WalletResult_t) - - // void free_string ( - // char * string); - fun free_string(string: Pointer?) - // BlockchainConfig_t * new_electrum_config ( // char const * url, // char const * socks5, @@ -118,6 +66,193 @@ interface LibJna : Library { // BlockchainConfig_t * blockchain_config); fun free_blockchain_config(blockchain_config: BlockchainConfig_t) + // typedef struct DatabaseConfig DatabaseConfig_t; + class DatabaseConfig_t : PointerType { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + // typedef struct OpaqueWallet OpaqueWallet_t; + class OpaqueWallet_t : PointerType { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + // typedef struct { + // + // OpaqueWallet_t * ok; + // + // char * * err; + // + // } FfiResult_OpaqueWallet_t; + open class FfiResult_OpaqueWallet_t : FfiResult() { + class ByValue : FfiResult_OpaqueWallet_t(), Structure.ByValue + class ByReference : FfiResult_OpaqueWallet_t(), Structure.ByReference + + override fun getFieldOrder() = listOf("ok", "err") + } + + // FfiResult_OpaqueWallet_t new_wallet_result ( + // char const * descriptor, + // char const * change_descriptor, + // BlockchainConfig_t const * blockchain_config, + // DatabaseConfig_t const * database_config); + fun new_wallet_result( + descriptor: String, + changeDescriptor: String?, + blockchainConfig: BlockchainConfig_t, + databaseConfig: DatabaseConfig_t, + ): FfiResult_OpaqueWallet_t.ByValue + + // void free_wallet_result ( + // FfiResult_OpaqueWallet_t wallet_result); + fun free_wallet_result(wallet_result: FfiResult_OpaqueWallet_t.ByValue) + + // typedef struct { + // + // char * txid; + // + // uint32_t vout; + // + // } OutPoint_t; + open class OutPoint_t : Structure() { + class ByValue : OutPoint_t(), Structure.ByValue + + @JvmField + var txid: String? = null + + @JvmField + var vout: Int? = null + + override fun getFieldOrder() = listOf("txid", "vout") + } + + // typedef struct { + // + // uint64_t value; + // + // char * script_pubkey; + // + // } TxOut_t; + open class TxOut_t : Structure() { + class ByValue : TxOut_t(), Structure.ByValue + + @JvmField + var value: Long? = null + + @JvmField + var script_pubkey: String? = null + + override fun getFieldOrder() = listOf("value", "script_pubkey") + } + + // typedef struct { + // + // OutPoint_t outpoint; + // + // TxOut_t txout; + // + // uint16_t keychain; + // + // } LocalUtxo_t; + open class LocalUtxo_t : Structure { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + + class ByValue : LocalUtxo_t, Structure.ByValue { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + class ByReference : LocalUtxo_t, Structure.ByReference { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + @JvmField + var outpoint: OutPoint_t? = null + + @JvmField + var txout: TxOut_t? = null + + @JvmField + var keychain: Short? = null + + override fun getFieldOrder() = listOf("outpoint", "txout", "keychain") + } + + // typedef struct { + // + // LocalUtxo_t * ptr; + // + // size_t len; + // + // size_t cap; + // + // } Vec_LocalUtxo_t; + open class Vec_LocalUtxo_t : Structure { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + + class ByReference : Vec_LocalUtxo_t, Structure.ByReference { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + class ByValue : Vec_LocalUtxo_t, Structure.ByValue { + constructor() : super() + constructor(pointer: Pointer) : super(pointer) + } + + @JvmField + var ptr: LocalUtxo_t.ByReference? = null + + @JvmField + var len: NativeLong? = null + + @JvmField + var cap: NativeLong? = null + + override fun getFieldOrder() = listOf("ptr", "len", "cap") + } + + // typedef struct { + // + // Vec_LocalUtxo_t ok; + // + // char * * err; + // + // } FfiResultVec_LocalUtxo_t; + open class FfiResultVec_LocalUtxo_t : Structure() { + + class ByValue : FfiResultVec_LocalUtxo_t(), Structure.ByValue + class ByReference : FfiResultVec_LocalUtxo_t(), Structure.ByReference + + @JvmField + var ok: Vec_LocalUtxo_t = Vec_LocalUtxo_t() + + @JvmField + var err: Pointer? = null + + override fun getFieldOrder() = listOf("ok", "err") + } + + // void free_unspent_result ( + // FfiResultVec_LocalUtxo_t unspent_result); + fun free_unspent_result(unspent_result: FfiResultVec_LocalUtxo_t.ByValue) + + // FfiResult_void_t sync_wallet ( + // OpaqueWallet_t const * opaque_wallet); + fun sync_wallet(opaque_wallet: OpaqueWallet_t): FfiResult_void_t.ByValue + + // FfiResult_char_ptr_t new_address ( + // OpaqueWallet_t const * opaque_wallet); + fun new_address(opaque_wallet: OpaqueWallet_t): FfiResult_char_ptr_t.ByValue + + // FfiResult_Vec_LocalUtxo_t list_unspent ( + // OpaqueWallet_t const * opaque_wallet); + fun list_unspent(opaque_wallet: OpaqueWallet_t): FfiResultVec_LocalUtxo_t.ByValue + // DatabaseConfig_t * new_memory_config (void); fun new_memory_config(): DatabaseConfig_t diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Result.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Result.kt new file mode 100644 index 0000000..6ceef7f --- /dev/null +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Result.kt @@ -0,0 +1,37 @@ +package org.bitcoindevkit.bdk + +import com.sun.jna.Pointer +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +abstract class Result(private val ffiResult: T): LibBase() { + + protected open val log: Logger = LoggerFactory.getLogger(Result::class.java) + + protected abstract fun getOkValue(pointer: Pointer): RT + + protected abstract fun freeResult(ffiResult: T) + + fun value(): RT { + val err = ffiResult.err + val ok = ffiResult.ok + when { + err != null -> { + val errString = err.getPointer(0).getString(0) + log.error("JnaError: $errString") + throw JnaException(JnaError.valueOf(errString)) + } + ok != null -> { + return getOkValue(ok) + } + else -> { + throw JnaException(JnaError.Generic) + } + } + } + + protected fun finalize() { + freeResult(ffiResult) + log.debug("$ffiResult freed") + } +} \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/StringResult.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/StringResult.kt index 6bfa121..29da3eb 100644 --- a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/StringResult.kt +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/StringResult.kt @@ -1,26 +1,15 @@ package org.bitcoindevkit.bdk import com.sun.jna.Pointer -import org.slf4j.Logger -import org.slf4j.LoggerFactory -class StringResult internal constructor(stringResultT: LibJna.StringResult_t) : - ResultBase(stringResultT) { +class StringResult constructor(stringResultPtr: LibJna.FfiResult_char_ptr_t.ByValue) : + Result(stringResultPtr) { - override val log: Logger = LoggerFactory.getLogger(StringResult::class.java) - - override fun err(pointerT: LibJna.StringResult_t): Pointer? { - return libJna.get_string_err(pointerT) + override fun getOkValue(pointer: Pointer): String { + return pointer.getPointer(0).getString(0) } - override fun ok(pointerT: LibJna.StringResult_t): String { - val okPointer = libJna.get_string_ok(pointerT) - val ok = okPointer!!.getString(0) - libJna.free_string(okPointer) - return ok - } - - override fun free(pointerT: LibJna.StringResult_t) { - libJna.free_string_result(pointerT) + override fun freeResult(ffiResult: LibJna.FfiResult_char_ptr_t.ByValue) { + libJna.free_string_result(ffiResult) } } \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VecLocalUtxoResult.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VecLocalUtxoResult.kt new file mode 100644 index 0000000..da69a1b --- /dev/null +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VecLocalUtxoResult.kt @@ -0,0 +1,31 @@ +package org.bitcoindevkit.bdk + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class VecLocalUtxoResult(private val ffiResult: LibJna.FfiResultVec_LocalUtxo_t.ByValue) : + LibBase() { + + protected open val log: Logger = LoggerFactory.getLogger(VecLocalUtxoResult::class.java) + + fun value(): Array { + val err = ffiResult.err + val ok = ffiResult.ok + when { + err != null -> { + val errString = err.getPointer(0).getString(0) + log.error("JnaError: $errString") + throw JnaException(JnaError.valueOf(errString)) + } + else -> { + val first = ok.ptr!! + return first.toArray(ok.len!!.toInt()) as Array + } + } + } + + protected fun finalize() { + libJna.free_unspent_result(ffiResult) + log.debug("$ffiResult freed") + } +} \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VoidResult.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VoidResult.kt index 5ed6c25..03b46be 100644 --- a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VoidResult.kt +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/VoidResult.kt @@ -1,23 +1,15 @@ package org.bitcoindevkit.bdk import com.sun.jna.Pointer -import org.slf4j.Logger -import org.slf4j.LoggerFactory -class VoidResult internal constructor(voidResultT: LibJna.VoidResult_t) : - ResultBase(voidResultT) { +class VoidResult constructor(voidResultPtr: LibJna.FfiResult_void_t.ByValue) : + Result(voidResultPtr) { - override val log: Logger = LoggerFactory.getLogger(VoidResult::class.java) - - override fun err(pointerT: LibJna.VoidResult_t): Pointer? { - return libJna.get_void_err(pointerT) + override fun getOkValue(pointer: Pointer) { + // No value } - override fun ok(pointerT: LibJna.VoidResult_t) { - // Void - } - - override fun free(pointerT: LibJna.VoidResult_t) { - libJna.free_void_result(pointerT) + override fun freeResult(ffiResult: LibJna.FfiResult_void_t.ByValue) { + libJna.free_void_result(ffiResult) } } \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Wallet.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Wallet.kt index d438778..7abea00 100644 --- a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Wallet.kt +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/Wallet.kt @@ -12,29 +12,28 @@ class Wallet constructor( val log: Logger = LoggerFactory.getLogger(Wallet::class.java) - private val walletResult = - WalletResult( - libJna.new_wallet_result( - descriptor, - changeDescriptor, - blockchainConfig.blockchainConfigT, - databaseConfig.databaseConfigT - ) + private val walletResult = WalletResult( + libJna.new_wallet_result( + descriptor, + changeDescriptor, + blockchainConfig.blockchainConfigT, + databaseConfig.databaseConfigT ) - private val walletRefT = walletResult.value() + ) + private val wallet = walletResult.value() fun sync() { - val voidResult = VoidResult(libJna.sync_wallet(walletRefT.pointer)) + val voidResult = VoidResult(libJna.sync_wallet(wallet)) return voidResult.value() } fun getAddress(): String { - val stringResult = StringResult(libJna.new_address(walletRefT.pointer)) + val stringResult = StringResult(libJna.new_address(wallet)) return stringResult.value() } - - protected fun finalize(walletRefT: LibJna.WalletRef_t) { - libJna.free_wallet_ref(walletRefT) - log.debug("$walletRefT freed") + + fun listUnspent(): Array { + val vecLocalUtxoResult = VecLocalUtxoResult(libJna.list_unspent(wallet)) + return vecLocalUtxoResult.value() } } \ No newline at end of file diff --git a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/WalletResult.kt b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/WalletResult.kt index 1b77cb8..ba8f979 100644 --- a/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/WalletResult.kt +++ b/bdk-kotlin/jvm/src/main/java/org/bitcoindevkit/bdk/WalletResult.kt @@ -1,23 +1,15 @@ package org.bitcoindevkit.bdk import com.sun.jna.Pointer -import org.slf4j.Logger -import org.slf4j.LoggerFactory -class WalletResult internal constructor(walletResultT: LibJna.WalletResult_t) : - ResultBase(walletResultT) { +class WalletResult constructor(walletResultPtr: LibJna.FfiResult_OpaqueWallet_t.ByValue) : + Result(walletResultPtr) { - override val log: Logger = LoggerFactory.getLogger(WalletResult::class.java) - - override fun err(pointerT: LibJna.WalletResult_t): Pointer? { - return libJna.get_wallet_err(pointerT) + override fun getOkValue(pointer: Pointer): LibJna.OpaqueWallet_t { + return LibJna.OpaqueWallet_t(pointer) } - override fun ok(pointerT: LibJna.WalletResult_t): LibJna.WalletRef_t { - return libJna.get_wallet_ok(pointerT)!! - } - - override fun free(pointerT: LibJna.WalletResult_t) { - libJna.free_wallet_result(pointerT) + override fun freeResult(ffiResult: LibJna.FfiResult_OpaqueWallet_t.ByValue) { + libJna.free_wallet_result(ffiResult) } } \ No newline at end of file diff --git a/bdk-kotlin/test-fixtures/src/main/java/org/bitcoindevkit/bdk/LibTest.kt b/bdk-kotlin/test-fixtures/src/main/java/org/bitcoindevkit/bdk/LibTest.kt index 3d0c5a0..5c84338 100644 --- a/bdk-kotlin/test-fixtures/src/main/java/org/bitcoindevkit/bdk/LibTest.kt +++ b/bdk-kotlin/test-fixtures/src/main/java/org/bitcoindevkit/bdk/LibTest.kt @@ -19,6 +19,9 @@ abstract class LibTest : LibBase() { val change = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)" + val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) + val databaseConfig = MemoryConfig() + abstract fun getTestDataDir(): String fun cleanupTestDataDir() { @@ -27,48 +30,69 @@ abstract class LibTest : LibBase() { @Test fun walletResultError() { - val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) - val databaseConfig = MemoryConfig() val jnaException = assertThrows(JnaException::class.java) { Wallet("bad", "bad", blockchainConfig, databaseConfig) } assertEquals(jnaException.err, JnaError.Descriptor) } - @Test - fun walletResultFinalize() { - val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) - val databaseConfig = MemoryConfig() - val names = listOf("one", "two", "three") - names.map { - val wallet = Wallet(desc, change, blockchainConfig, databaseConfig) - assertNotNull(wallet) - } - System.gc() - // The only way to verify wallets freed is by checking the log - } +// @Test +// fun walletResultFinalize() { +// run { +// val desc = +// "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)" +// val change = +// "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)" +// +// val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) +// val databaseConfig = MemoryConfig() +// val wallet = Wallet(desc, change, blockchainConfig, databaseConfig) +// wallet.sync() +// assertNotNull(wallet.getAddress()) +// } +// System.gc() +// Thread.sleep(2000) +// // The only way to verify wallets freed is by checking the log +// } @Test fun walletSync() { val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) val testDataDir = getTestDataDir() - log.debug("testDataDir = $testDataDir") + // log.debug("testDataDir = $testDataDir") val databaseConfig = SledConfig(testDataDir, "steve-test") val wallet = Wallet(desc, change, blockchainConfig, databaseConfig) - assertNotNull(wallet) wallet.sync() cleanupTestDataDir() } @Test fun walletNewAddress() { - val blockchainConfig = ElectrumConfig("ssl://electrum.blockstream.info:60002", null, 5, 30) - val databaseConfig = MemoryConfig() val wallet = Wallet(desc, change, blockchainConfig, databaseConfig) - assertNotNull(wallet) val address = wallet.getAddress() assertNotNull(address) - log.debug("address created from kotlin: $address") + // log.debug("address created from kotlin: $address") assertEquals(address, "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e") } + + @Test + fun walletUnspent() { + val wallet = Wallet(desc, change, blockchainConfig, databaseConfig) + wallet.sync() + val unspent = wallet.listUnspent() + assertTrue(unspent.isNotEmpty()) + + unspent.iterator().forEach { + //log.debug("unspent.outpoint.txid: ${it.outpoint!!.txid}") + assertNotNull(it.outpoint?.txid) + //log.debug("unspent.outpoint.vout: ${it.outpoint?.vout}") + assertTrue(it.outpoint?.vout!! >= 0) + //log.debug("unspent.txout.value: ${it.txout?.value}") + assertTrue(it.txout?.value!! > 0) + //log.debug("unspent.txout.script_pubkey: ${it.txout?.script_pubkey}") + assertNotNull(it.txout?.script_pubkey) + //log.debug("unspent.keychain: ${it.keychain}") + assertTrue(it.keychain!! >= 0) + } + } } diff --git a/build.sh b/build.sh index bbcc9bf..6b5a530 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,7 @@ set -eo pipefail -o xtrace # rust +cargo fmt cargo build cargo test --features c-headers -- generate_headers diff --git a/cc/bdk_ffi.h b/cc/bdk_ffi.h new file mode 100644 index 0000000..eba536a --- /dev/null +++ b/cc/bdk_ffi.h @@ -0,0 +1,154 @@ +/*! \file */ +/******************************************* + * * + * File auto-generated by `::safer_ffi`. * + * * + * Do not manually edit this file. * + * * + *******************************************/ + +#ifndef __RUST_BDK_FFI__ +#define __RUST_BDK_FFI__ + +#ifdef __cplusplus +extern "C" { +#endif + + +#include +#include + +typedef struct BlockchainConfig BlockchainConfig_t; + +BlockchainConfig_t * new_electrum_config ( + char const * url, + char const * socks5, + int16_t retry, + int16_t timeout); + +void free_blockchain_config ( + BlockchainConfig_t * blockchain_config); + +typedef struct DatabaseConfig DatabaseConfig_t; + +typedef struct OpaqueWallet OpaqueWallet_t; + +typedef struct { + + OpaqueWallet_t * ok; + + char * * err; + +} FfiResult_OpaqueWallet_t; + +FfiResult_OpaqueWallet_t new_wallet_result ( + char const * descriptor, + char const * change_descriptor, + BlockchainConfig_t const * blockchain_config, + DatabaseConfig_t const * database_config); + +void free_wallet_result ( + FfiResult_OpaqueWallet_t wallet_result); + +typedef struct { + + char * txid; + + uint32_t vout; + +} OutPoint_t; + +typedef struct { + + uint64_t value; + + char * script_pubkey; + +} TxOut_t; + +typedef struct { + + OutPoint_t outpoint; + + TxOut_t txout; + + uint16_t keychain; + +} LocalUtxo_t; + +/** \brief + * Same as [`Vec`][`rust::Vec`], but with guaranteed `#[repr(C)]` layout + */ +typedef struct { + + LocalUtxo_t * ptr; + + size_t len; + + size_t cap; + +} Vec_LocalUtxo_t; + +typedef struct { + + Vec_LocalUtxo_t ok; + + char * * err; + +} FfiResultVec_LocalUtxo_t; + +void free_unspent_result ( + FfiResultVec_LocalUtxo_t unspent_result); + +typedef struct { + + void * ok; + + char * * err; + +} FfiResult_void_t; + +FfiResult_void_t sync_wallet ( + OpaqueWallet_t const * opaque_wallet); + +typedef struct { + + char * * ok; + + char * * err; + +} FfiResult_char_ptr_t; + +FfiResult_char_ptr_t new_address ( + OpaqueWallet_t const * opaque_wallet); + +FfiResultVec_LocalUtxo_t list_unspent ( + OpaqueWallet_t const * opaque_wallet); + +DatabaseConfig_t * new_memory_config (void); + +DatabaseConfig_t * new_sled_config ( + char const * path, + char const * tree_name); + +void free_database_config ( + DatabaseConfig_t * database_config); + +void free_string_result ( + FfiResult_char_ptr_t string_result); + +void free_void_result ( + FfiResult_void_t void_result); + +/** \brief + * Frees a Rust-allocated string + */ +void free_string ( + char * string); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* __RUST_BDK_FFI__ */ diff --git a/cc/bdk_ffi_test.c b/cc/bdk_ffi_test.c index 487b20f..8ea5590 100644 --- a/cc/bdk_ffi_test.c +++ b/cc/bdk_ffi_test.c @@ -12,19 +12,20 @@ int main (int argc, char const * const argv[]) //DatabaseConfig_t *db_config = new_sled_config("/home/steve/.bdk", "test_wallet"); DatabaseConfig_t *db_config = new_memory_config(); - WalletResult_t *wallet_result = new_wallet_result("bad", "bad", bc_config, db_config); - assert(wallet_result != NULL); + // new wallet with bad descriptor + FfiResult_OpaqueWallet_t wallet_result = new_wallet_result("bad","bad",bc_config,db_config); + assert(wallet_result.err != NULL); + assert(wallet_result.ok == NULL); + free_blockchain_config(bc_config); free_database_config(db_config); - char *wallet_err = get_wallet_err(wallet_result); + + char *wallet_err = *wallet_result.err; assert(wallet_err != NULL); assert( 0 == strcmp(wallet_err,"Descriptor") ); - //printf("wallet err: %s\n", wallet_err); - WalletRef_t *wallet_ref = get_wallet_ok(wallet_result); - assert(wallet_ref == NULL); - free_string(wallet_err); + // printf("wallet err: %s\n", wallet_err); + free_wallet_result(wallet_result); - } // test new wallet @@ -33,51 +34,94 @@ int main (int argc, char const * const argv[]) char const *change = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"; BlockchainConfig_t *bc_config = new_electrum_config("ssl://electrum.blockstream.info:60002", NULL, 5, 30); - //DatabaseConfig_t *db_config = new_sled_config("/home/steve/.bdk", "test_wallet"); DatabaseConfig_t *db_config = new_memory_config(); - WalletResult_t *wallet_result = new_wallet_result(desc, change, bc_config, db_config); - assert(wallet_result != NULL); + // new wallet + FfiResult_OpaqueWallet_t wallet_result = new_wallet_result(desc,change,bc_config,db_config); + assert(wallet_result.err == NULL); + assert(wallet_result.ok != NULL); + free_blockchain_config(bc_config); free_database_config(db_config); - char *wallet_err = get_wallet_err(wallet_result); - assert(wallet_err == NULL); - WalletRef_t *wallet_ref = get_wallet_ok(wallet_result); - assert(wallet_ref != NULL); - // test sync_wallet - VoidResult_t *sync_result = sync_wallet(wallet_ref); - free_void_result(sync_result); + OpaqueWallet_t *wallet = wallet_result.ok; - // test new_address - StringResult_t *address_result1 = new_address(wallet_ref); - char *address1 = get_string_ok(address_result1); - //printf("address1: %s\n", address1); - assert( 0 == strcmp(address1,"tb1qgkhp034fyxeta00h0nne9tzfm0vsxq4prduzxp")); - free_string(address1); + // test sync wallet + FfiResult_void_t sync_result = sync_wallet(wallet); + assert(sync_result.ok != NULL); + assert(sync_result.err == NULL); + free_void_result(sync_result); + + // test new address + FfiResult_char_ptr_t address_result1 = new_address(wallet); + assert(address_result1.ok != NULL); + assert(address_result1.err == NULL); + // printf("address1 = %s\n", *address_result1.ok); + assert( 0 == strcmp(*address_result1.ok,"tb1qgkhp034fyxeta00h0nne9tzfm0vsxq4prduzxp")); free_string_result(address_result1); - StringResult_t *address_result2 = new_address(wallet_ref); - char *address2 = get_string_ok(address_result2); - //printf("address2: %s\n", address2); - assert(0 == strcmp(address2,"tb1qd6u9q327sru2ljvwzdtfrdg36sapax7udz97wf")); - free_string(address2); + FfiResult_char_ptr_t address_result2 = new_address(wallet); + assert(address_result2.ok != NULL); + assert(address_result2.err == NULL); + // printf("address2 = %s\n", *address_result2.ok); + assert( 0 == strcmp(*address_result2.ok,"tb1qd6u9q327sru2ljvwzdtfrdg36sapax7udz97wf")); free_string_result(address_result2); - free_wallet_ref(wallet_ref); - // test free_wallet free_wallet_result(wallet_result); - // test free_wallet NULL doesn't crash - free_wallet_result(NULL); - // verify free_wallet after free_wallet fails (core dumped) - ////free_wallet_result(wallet_result); + //// free_wallet_result(wallet_result); // verify sync_wallet after free_wallet fails (core dumped) - ////VoidResult_t sync_result2 = sync_wallet(wallet_result); - + //// FfiResult_void_t sync_result2 = sync_wallet(wallet); + } + + // test get unspent utxos + { + char const *desc = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)"; + char const *change = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"; + + BlockchainConfig_t *bc_config = new_electrum_config("ssl://electrum.blockstream.info:60002", NULL, 5, 30); + DatabaseConfig_t *db_config = new_memory_config(); + + // new wallet + FfiResult_OpaqueWallet_t wallet_result = new_wallet_result(desc,change,bc_config,db_config); + assert(wallet_result.err == NULL); + assert(wallet_result.ok != NULL); + + free_blockchain_config(bc_config); + free_database_config(db_config); + + OpaqueWallet_t *wallet = wallet_result.ok; + + // test sync wallet + FfiResult_void_t sync_result = sync_wallet(wallet); + assert(sync_result.ok != NULL); + assert(sync_result.err == NULL); + free_void_result(sync_result); + + // list unspent + FfiResultVec_LocalUtxo_t unspent_result = list_unspent(wallet); + assert(unspent_result.ok.len == 7); + assert(unspent_result.err == NULL); + + LocalUtxo_t * unspent_ptr = unspent_result.ok.ptr; + for (int i = 0; i < unspent_result.ok.len; i++) { + // printf("%d: outpoint.txid: %s\n", i, unspent_ptr[i].outpoint.txid); + assert(unspent_ptr[i].outpoint.txid != NULL); + // printf("%d: outpoint.vout: %d\n", i, unspent_ptr[i].outpoint.vout); + assert(unspent_ptr[i].outpoint.vout >= 0); + // printf("%d: txout.value: %ld\n", i, unspent_ptr[i].txout.value); + assert(unspent_ptr[i].txout.value > 0); + // printf("%d: txout.script_pubkey: %s\n", i, unspent_ptr[i].txout.script_pubkey); + assert(unspent_ptr[i].txout.script_pubkey != NULL); + // printf("%d: keychain: %d\n", i, unspent_ptr[i].keychain); + assert(unspent_ptr[i].keychain >= 0); + } + + free_unspent_result(unspent_result); + free_wallet_result(wallet_result); } return EXIT_SUCCESS; diff --git a/src/lib.rs b/src/lib.rs index 72e9910..e1a9c00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod blockchain; mod database; mod error; +mod types; mod wallet; /// The following test function is necessary for the header generation. diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..25a1f2a --- /dev/null +++ b/src/types.rs @@ -0,0 +1,34 @@ +use ::safer_ffi::prelude::*; +use safer_ffi::char_p::char_p_boxed; + +#[derive_ReprC] +#[repr(C)] +#[derive(Debug)] +pub struct FfiResult { + pub ok: Option>, + pub err: Option>, +} + +#[derive_ReprC] +#[repr(C)] +pub struct FfiResultVec { + pub ok: repr_c::Vec, + pub err: Option>, +} + +#[ffi_export] +fn free_string_result(string_result: FfiResult) { + drop(string_result) +} + +#[ffi_export] +fn free_void_result(void_result: FfiResult<()>) { + drop(void_result) +} + +// TODO do we need this? remove? +/// Frees a Rust-allocated string +#[ffi_export] +fn free_string(string: Option) { + drop(string) +} diff --git a/src/wallet.rs b/src/wallet.rs index 454f547..b7996dc 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,82 +1,23 @@ -use crate::blockchain::BlockchainConfig; -use crate::database::DatabaseConfig; -use crate::error::get_name; +use std::convert::TryFrom; + use ::safer_ffi::prelude::*; use bdk::bitcoin::network::constants::Network::Testnet; -use bdk::blockchain::{ - log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, - ElectrumBlockchainConfig, -}; +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}; -#[derive_ReprC] -#[ReprC::opaque] -pub struct VoidResult { - raw: Result<(), Error>, -} - -#[ffi_export] -fn get_void_err(void_result: &VoidResult) -> Option { - void_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(e)).unwrap()) -} - -#[ffi_export] -fn free_void_result(void_result: Option>) { - drop(void_result) -} +use crate::blockchain::BlockchainConfig; +use crate::database::DatabaseConfig; +use crate::error::get_name; +use crate::types::{FfiResult, FfiResultVec}; #[derive_ReprC] #[ReprC::opaque] -pub struct StringResult { - raw: Result, -} - -#[ffi_export] -fn get_string_ok(string_result: &StringResult) -> Option { - string_result - .raw - .as_ref() - .ok() - .map(|s| char_p_boxed::try_from(s.clone()).unwrap()) -} - -#[ffi_export] -fn get_string_err(string_result: &StringResult) -> Option { - string_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(e)).unwrap()) -} - -#[ffi_export] -fn free_string_result(string_result: Option>) { - drop(string_result) -} - -#[derive_ReprC] -#[ReprC::opaque] -pub struct WalletRef<'lt> { - raw: &'lt Wallet, -} - -#[ffi_export] -fn free_wallet_ref(wallet_ref: Option>) { - drop(wallet_ref) -} - -#[derive_ReprC] -#[ReprC::opaque] -pub struct WalletResult { - raw: Result, Error>, +pub struct OpaqueWallet { + raw: Wallet, } #[ffi_export] @@ -85,13 +26,83 @@ fn new_wallet_result( change_descriptor: Option, blockchain_config: &BlockchainConfig, database_config: &DatabaseConfig, -) -> Box { +) -> FfiResult { let descriptor = descriptor.to_string(); let change_descriptor = change_descriptor.map(|s| s.to_string()); let bc_config = &blockchain_config.raw; let db_config = &database_config.raw; let wallet_result = new_wallet(descriptor, change_descriptor, bc_config, db_config); - Box::new(WalletResult { raw: wallet_result }) + + match wallet_result { + Ok(w) => FfiResult { + ok: Some(Box::new(OpaqueWallet { raw: w })), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[ffi_export] +fn free_wallet_result(wallet_result: FfiResult) { + drop(wallet_result); +} + +#[ffi_export] +fn free_unspent_result(unspent_result: FfiResultVec) { + drop(unspent_result) +} + +#[ffi_export] +fn sync_wallet(opaque_wallet: &OpaqueWallet) -> FfiResult<()> { + let void_result = opaque_wallet.raw.sync(log_progress(), Some(100)); + match void_result { + Ok(v) => FfiResult { + ok: Some(Box::new(v)), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[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: Some(Box::new(char_p_boxed::try_from(a).unwrap())), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[ffi_export] +fn list_unspent(opaque_wallet: &OpaqueWallet) -> FfiResultVec { + let unspent_result = opaque_wallet.raw.list_unspent(); + + match unspent_result { + Ok(v) => FfiResultVec { + ok: { + let ve: Vec = v.iter().map(|lu| LocalUtxo::from(lu)).collect(); + repr_c::Vec::from(ve) + }, + err: None, + }, + Err(e) => FfiResultVec { + ok: repr_c::Vec::EMPTY, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } } fn new_wallet( @@ -111,44 +122,64 @@ fn new_wallet( Wallet::new(descriptor, change_descriptor, network, database, client) } -#[ffi_export] -fn get_wallet_err(wallet_result: &WalletResult) -> Option { - wallet_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(&e)).unwrap()) +// Non-opaque returned structs + +#[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, } -#[ffi_export] -fn get_wallet_ok<'lt>(wallet_result: &'lt WalletResult) -> Option>> { - wallet_result - .raw - .as_ref() - .ok() - .map(|w| Box::new(WalletRef { raw: w })) +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, + } + } } -#[ffi_export] -fn sync_wallet<'lt>(wallet_ref: &'lt WalletRef<'lt>) -> Box { - let void_result = wallet_ref.raw.sync(log_progress(), Some(100)); - Box::new(VoidResult { raw: void_result }) +#[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, } -#[ffi_export] -fn new_address<'lt>(wallet_ref: &'lt WalletRef<'lt>) -> Box { - let new_address = wallet_ref.raw.get_address(New); - let string_result = new_address.map(|a| a.to_string()); - Box::new(StringResult { raw: string_result }) +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(), + } + } } -#[ffi_export] -fn free_wallet_result(wallet_result: Option>) { - drop(wallet_result) +#[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, } -/// Frees a Rust-allocated string -#[ffi_export] -fn free_string(string: Option) { - drop(string) +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, + } + } } diff --git a/test.sh b/test.sh index 03855c5..99a3ac8 100755 --- a/test.sh +++ b/test.sh @@ -6,9 +6,9 @@ cargo test --features c-headers -- generate_headers # cc export LD_LIBRARY_PATH=`pwd`/target/debug -#valgrind --leak-check=full --show-leak-kinds=all cc/bdk_ffi_test -cc/bdk_ffi_test +valgrind --leak-check=full --show-leak-kinds=all cc/bdk_ffi_test +#cc/bdk_ffi_test -## bdk-kotlin +# bdk-kotlin (cd bdk-kotlin && gradle test) (cd bdk-kotlin && gradle :android:connectedDebugAndroidTest)