diff --git a/.github/workflows/test-android.yaml b/.github/workflows/test-android.yaml index c5ef1bcc..48abe6b1 100644 --- a/.github/workflows/test-android.yaml +++ b/.github/workflows/test-android.yaml @@ -55,11 +55,12 @@ jobs: - name: "Build Android library" run: | cd bdk-android - ./gradlew buildAndroidLib + ./gradlew buildAndroidLib --console=plain -# There are currently no unit tests for bdk-android and the integration tests require the macOS image -# which is not working with the older NDK version we are using, so for now we just make sure that the library builds. -# - name: "Run Android unit tests" +# There are currently no unit tests for bdk-android (see the tests in bdk-jvm instead) and the +# integration tests require the macOS image which is not working with the older NDK version we +# are using, so for now we just make sure that the library builds and omit the connectedTest +# - name: "Run Android connected tests" # run: | # cd bdk-android -# ./gradlew test --console=rich +# ./gradlew connectedAndroidTest --console=plain diff --git a/.github/workflows/test-jvm.yaml b/.github/workflows/test-jvm.yaml index caf07a54..e2c0aa8c 100644 --- a/.github/workflows/test-jvm.yaml +++ b/.github/workflows/test-jvm.yaml @@ -38,4 +38,4 @@ jobs: - name: "Run JVM tests" run: | cd bdk-jvm - ./gradlew test + ./gradlew test -P excludeConnectedTests diff --git a/.github/workflows/test-python.yaml b/.github/workflows/test-python.yaml index 8d6b3691..0108e7aa 100644 --- a/.github/workflows/test-python.yaml +++ b/.github/workflows/test-python.yaml @@ -54,7 +54,7 @@ jobs: run: ${PYBIN}/pip install ./dist/*.whl - name: "Run tests" - run: ${PYBIN}/python -m unittest tests/test_bdk.py --verbose + run: ${PYBIN}/python -m unittest discover --start "./tests/" --pattern "test_offline_*.py" --verbose - name: "Upload artifact test" uses: actions/upload-artifact@v3 @@ -97,7 +97,7 @@ jobs: # - name: "Install wheel and run tests" # run: | # pip3 install ./dist/*.whl - # python3 -m unittest tests/test_bdk.py --verbose + # python3 -m unittest discover --start "./tests/" --pattern "test_offline_*.py" --verbose - name: "Upload artifact test" uses: actions/upload-artifact@v3 @@ -138,7 +138,7 @@ jobs: run: pip3 install ./dist/*.whl - name: "Run tests" - run: python3 -m unittest tests/test_bdk.py --verbose + run: python3 -m unittest discover --start "./tests/" --pattern "test_offline_*.py" --verbose - name: "Upload artifact test" uses: actions/upload-artifact@v3 @@ -186,4 +186,4 @@ jobs: shell: powershell - name: "Run tests" - run: python -m unittest tests/test_bdk.py --verbose + run: python -m unittest discover --start "./tests/" --pattern "test_offline_*.py" --verbose diff --git a/.github/workflows/test-swift.yaml b/.github/workflows/test-swift.yaml index fad668f7..d74728a2 100644 --- a/.github/workflows/test-swift.yaml +++ b/.github/workflows/test-swift.yaml @@ -23,4 +23,4 @@ jobs: - name: "Run Swift tests" working-directory: bdk-swift - run: swift test + run: swift test --skip LiveWalletTests --skip LiveTxBuilderTests diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt new file mode 100644 index 00000000..6b72e06e --- /dev/null +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt @@ -0,0 +1,30 @@ +package org.bitcoindevkit + +import org.junit.Test +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class LiveTxBuilderTest { + @Test + fun testTxBuilder() { + val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) + val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET) + val esploraClient = EsploraClient("https://mempool.space/testnet/api") + val update = esploraClient.scan(wallet, 10uL, 1uL) + wallet.applyUpdate(update) + println("Balance: ${wallet.getBalance().total()}") + + assert(wallet.getBalance().total() > 0uL) + + val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) + val psbt: PartiallySignedTransaction = TxBuilder() + .addRecipient(recipient.scriptPubkey(), 4200uL) + .feeRate(2.0f) + .finish(wallet) + + println(psbt.serialize()) + assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") + } +} diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt new file mode 100644 index 00000000..0fd4fb30 --- /dev/null +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt @@ -0,0 +1,21 @@ +package org.bitcoindevkit + +import org.junit.Test +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LiveWalletTest { + @Test + fun testSyncedBalance() { + val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) + val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET) + val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api") + // val esploraClient = EsploraClient("https://blockstream.info/testnet/api") + val update = esploraClient.scan(wallet, 10uL, 1uL) + wallet.applyUpdate(update) + println("Balance: ${wallet.getBalance().total()}") + + assert(wallet.getBalance().total() > 0uL) + } +} diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineDescriptorTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineDescriptorTest.kt new file mode 100644 index 00000000..22fcb717 --- /dev/null +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineDescriptorTest.kt @@ -0,0 +1,21 @@ +package org.bitcoindevkit + +import kotlin.test.Test +import kotlin.test.assertTrue +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OfflineDescriptorTest { + @Test + fun testDescriptorBip86() { + val mnemonic: Mnemonic = Mnemonic.fromString("space echo position wrist orient erupt relief museum myself grain wisdom tumble") + val descriptorSecretKey: DescriptorSecretKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) + val descriptor: Descriptor = Descriptor.newBip86(descriptorSecretKey, KeychainKind.EXTERNAL, Network.TESTNET) + + assertEquals( + expected = "tr([be1eec8f/86'/1'/0']tpubDCTtszwSxPx3tATqDrsSyqScPNnUChwQAVAkanuDUCJQESGBbkt68nXXKRDifYSDbeMa2Xg2euKbXaU3YphvGWftDE7ozRKPriT6vAo3xsc/0/*)#m7puekcx", + actual = descriptor.asString() + ) + } +} diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt similarity index 83% rename from bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt rename to bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt index 394c60f1..5a3206a0 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/AndroidLibTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt @@ -1,19 +1,13 @@ package org.bitcoindevkit -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.runner.RunWith import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ @RunWith(AndroidJUnit4::class) -class WalletTest { - +class OfflineWalletTest { @Test fun testDescriptorBip86() { val mnemonic: Mnemonic = Mnemonic(WordCount.WORDS12) @@ -36,7 +30,10 @@ class WalletTest { ) val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) - assertEquals("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", addressInfo.address.asString()) + assertEquals( + expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", + actual = addressInfo.address.asString() + ) } @Test @@ -51,7 +48,9 @@ class WalletTest { Network.TESTNET ) - assertEquals(0uL, wallet.getBalance().total()) + assertEquals( + expected = 0uL, + actual = wallet.getBalance().total() + ) } - -} \ No newline at end of file +} diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index af03f6c8..caabbaaf 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -1,7 +1,7 @@ namespace bdk {}; // ------------------------------------------------------------------------ -// bdk crate +// bdk crate - root module // ------------------------------------------------------------------------ enum KeychainKind { @@ -76,16 +76,31 @@ interface Wallet { AddressInfo get_address(AddressIndex address_index); + AddressInfo get_internal_address(AddressIndex address_index); + Network network(); Balance get_balance(); + boolean is_mine(Script script); + [Throws=BdkError] void apply_update(Update update); }; interface Update {}; +interface TxBuilder { + constructor(); + + TxBuilder add_recipient(Script script, u64 amount); + + TxBuilder fee_rate(float sat_per_vbyte); + + [Throws=BdkError] + PartiallySignedTransaction finish([ByRef] Wallet wallet); +}; + // ------------------------------------------------------------------------ // bdk crate - descriptor module // ------------------------------------------------------------------------ @@ -214,7 +229,37 @@ interface Address { Network network(); + Script script_pubkey(); + string to_qr_uri(); string as_string(); }; + +interface Transaction { + [Throws=BdkError] + constructor(sequence transaction_bytes); + + string txid(); + + u64 size(); + + u64 vsize(); + + boolean is_coin_base(); + + boolean is_explicitly_rbf(); + + boolean is_lock_time_enabled(); + + i32 version(); +}; + +interface PartiallySignedTransaction { + [Throws=BdkError] + constructor(string psbt_base64); + + string serialize(); + + Transaction extract_tx(); +}; diff --git a/bdk-ffi/src/bitcoin.rs b/bdk-ffi/src/bitcoin.rs index 587e6e74..2730e20f 100644 --- a/bdk-ffi/src/bitcoin.rs +++ b/bdk-ffi/src/bitcoin.rs @@ -1,4 +1,15 @@ +use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; +use bdk::bitcoin::consensus::Decodable; +use bdk::bitcoin::network::constants::Network as BdkNetwork; +use bdk::bitcoin::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction; +use bdk::bitcoin::Address as BdkAddress; +use bdk::bitcoin::Transaction as BdkTransaction; +use bdk::Error as BdkError; + +use std::io::Cursor; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; /// A Bitcoin script. #[derive(Clone, Debug, PartialEq, Eq)] @@ -20,3 +31,251 @@ impl From for Script { Script(script) } } + +pub enum Network { + /// Mainnet Bitcoin. + Bitcoin, + /// Bitcoin's testnet network. + Testnet, + /// Bitcoin's signet network. + Signet, + /// Bitcoin's regtest network. + Regtest, +} + +impl From for BdkNetwork { + fn from(network: Network) -> Self { + match network { + Network::Bitcoin => BdkNetwork::Bitcoin, + Network::Testnet => BdkNetwork::Testnet, + Network::Signet => BdkNetwork::Signet, + Network::Regtest => BdkNetwork::Regtest, + } + } +} + +impl From for Network { + fn from(network: BdkNetwork) -> Self { + match network { + BdkNetwork::Bitcoin => Network::Bitcoin, + BdkNetwork::Testnet => Network::Testnet, + BdkNetwork::Signet => Network::Signet, + BdkNetwork::Regtest => Network::Regtest, + _ => panic!("Network {} not supported", network), + } + } +} + +/// A Bitcoin address. +#[derive(Debug, PartialEq, Eq)] +pub struct Address { + inner: BdkAddress, +} + +impl Address { + pub fn new(address: String, network: Network) -> Result { + Ok(Address { + inner: address + .parse::>() + .unwrap() // TODO 11: Handle error correctly by rethrowing it as a BdkError + .require_network(network.into()) + .map_err(|e| BdkError::Generic(e.to_string()))?, + }) + } + + /// alternative constructor + // fn from_script(script: Arc