diff --git a/.gitignore b/.gitignore index 85015764..a7699b89 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ bdk.kt # Swift related /.build -/.swiftpm +.swiftpm /Packages /*.xcodeproj xcuserdata/ diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt index 73153634..d3869529 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt @@ -3,6 +3,7 @@ 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 LiveWalletTest { @@ -20,4 +21,38 @@ class LiveWalletTest { assert(wallet.getBalance().total() > 0uL) } + + @Test + fun testBroadcastTransaction() { + 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()}") + println("New address: ${wallet.getAddress(AddressIndex.New).address}") + + assert(wallet.getBalance().total() > 0uL) { + "Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again." + } + + val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) + + val psbt: PartiallySignedTransaction = TxBuilder() + .addRecipient(recipient.scriptPubkey(), 4200uL) + .feeRate(4.0f) + .finish(wallet) + + println(psbt.serialize()) + assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") + + val walletDidSign = wallet.sign(psbt) + assertTrue(walletDidSign) + + val tx: Transaction = psbt.extractTx() + + println("Txid is: ${tx.txid()}") + esploraClient.broadcast(tx) + } } diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index 2af3df8c..58acc0f5 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -221,6 +221,9 @@ interface EsploraClient { [Throws=BdkError] Update scan(Wallet wallet, u64 stop_gap, u64 parallel_requests); + + [Throws=BdkError] + void broadcast(Transaction transaction); }; // ------------------------------------------------------------------------ diff --git a/bdk-ffi/src/bitcoin.rs b/bdk-ffi/src/bitcoin.rs index a12b7133..72b41ceb 100644 --- a/bdk-ffi/src/bitcoin.rs +++ b/bdk-ffi/src/bitcoin.rs @@ -209,6 +209,12 @@ impl From for Transaction { } } +impl From for BdkTransaction { + fn from(tx: Transaction) -> Self { + tx.inner + } +} + pub struct PartiallySignedTransaction { pub(crate) inner: Mutex, } diff --git a/bdk-ffi/src/esplora.rs b/bdk-ffi/src/esplora.rs index 284f368e..e81ac8ac 100644 --- a/bdk-ffi/src/esplora.rs +++ b/bdk-ffi/src/esplora.rs @@ -1,4 +1,5 @@ use crate::wallet::{Update, Wallet}; +use std::ops::Deref; use bdk::wallet::Update as BdkUpdate; use bdk::Error as BdkError; @@ -56,7 +57,12 @@ impl EsploraClient { // pub fn sync(); - // pub fn broadcast(); + pub fn broadcast(&self, transaction: Arc) -> Result<(), BdkError> { + let bdk_transaction: bdk::bitcoin::Transaction = transaction.deref().clone().into(); + self.0 + .broadcast(&bdk_transaction) + .map_err(|e| BdkError::Generic(e.to_string())) + } // pub fn estimate_fee(); } diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index b9c38f79..5778e55f 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -7,8 +7,8 @@ use std::collections::HashSet; use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; use bdk::bitcoin::OutPoint as BdkOutPoint; use bdk::wallet::Update as BdkUpdate; -use bdk::{SignOptions, Wallet as BdkWallet}; use bdk::{Error as BdkError, FeeRate}; +use bdk::{SignOptions, Wallet as BdkWallet}; use bdk::wallet::tx_builder::ChangeSpendPolicy; use std::sync::{Arc, Mutex, MutexGuard}; @@ -88,10 +88,9 @@ impl Wallet { // sign_options: Option, ) -> Result { let mut psbt = psbt.inner.lock().unwrap(); - self.get_wallet().sign( - &mut psbt, - SignOptions::default(), - ).map_err(|e| BdkError::Generic(e.to_string())) + self.get_wallet() + .sign(&mut psbt, SignOptions::default()) + .map_err(|e| BdkError::Generic(e.to_string())) } } diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt index 44f6dca4..390cb6e2 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt @@ -1,6 +1,7 @@ package org.bitcoindevkit import kotlin.test.Test +import kotlin.test.assertTrue class LiveWalletTest { @Test @@ -15,4 +16,38 @@ class LiveWalletTest { assert(wallet.getBalance().total() > 0uL) } + + @Test + fun testBroadcastTransaction() { + 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()}") + println("New address: ${wallet.getAddress(AddressIndex.New).address.asString()}") + + assert(wallet.getBalance().total() > 0uL) { + "Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again." + } + + 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'") + + val walletDidSign = wallet.sign(psbt) + assertTrue(walletDidSign) + + val tx: Transaction = psbt.extractTx() + + println("Txid is: ${tx.txid()}") + esploraClient.broadcast(tx) + } } diff --git a/bdk-python/tests/test_live_wallet.py b/bdk-python/tests/test_live_wallet.py index 16ee1e90..9caa4483 100644 --- a/bdk-python/tests/test_live_wallet.py +++ b/bdk-python/tests/test_live_wallet.py @@ -23,6 +23,41 @@ def test_synced_balance(self): self.assertGreater(wallet.get_balance().total(), 0) + def test_broadcast_transaction(self): + descriptor: bdk.Descriptor = bdk.Descriptor( + "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", + bdk.Network.TESTNET + ) + wallet: bdk.Wallet = bdk.Wallet.new_no_persist( + descriptor, + None, + bdk.Network.TESTNET + ) + esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api") + update = esploraClient.scan( + wallet = wallet, + stop_gap = 10, + parallel_requests = 1 + ) + wallet.apply_update(update) + + self.assertGreater(wallet.get_balance().total(), 0) + + recipient = bdk.Address( + address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", + network = bdk.Network.TESTNET + ) + + psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(2.0).finish(wallet) + # print(psbt.serialize()) + self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") + + walletDidSign = wallet.sign(psbt) + self.assertTrue(walletDidSign) + tx = psbt.extract_tx() + + esploraClient.broadcast(tx) + if __name__ == '__main__': unittest.main() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift index d0aee18e..a0ef5dc4 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift @@ -4,7 +4,7 @@ import XCTest final class LiveTxBuilderTests: XCTestCase { func testTxBuilder() throws { let descriptor = try Descriptor( - descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", + descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", network: Network.testnet ) let wallet = try Wallet.newNoPersist( diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift index b8b6fabd..09788de8 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift @@ -4,7 +4,7 @@ import XCTest final class LiveWalletTests: XCTestCase { func testSyncedBalance() throws { let descriptor = try Descriptor( - descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", + descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", network: Network.testnet ) let wallet = try Wallet.newNoPersist( @@ -22,4 +22,44 @@ final class LiveWalletTests: XCTestCase { XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0)) } + + func testBroadcastTransaction() throws { + let descriptor = try Descriptor( + descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", + network: Network.testnet + ) + let wallet = try Wallet.newNoPersist( + descriptor: descriptor, + changeDescriptor: nil, + network: .testnet + ) + let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api") + let update = try esploraClient.scan( + wallet: wallet, + stopGap: 10, + parallelRequests: 1 + ) + try wallet.applyUpdate(update: update) + + XCTAssertGreaterThan(wallet.getBalance().total(), UInt64(0), "Wallet must have positive balance, please add funds") + + print("Balance: \(wallet.getBalance().total())") + + let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) + let psbt: PartiallySignedTransaction = try + TxBuilder() + .addRecipient(script: recipient.scriptPubkey(), amount: 4200) + .feeRate(satPerVbyte: 2.0) + .finish(wallet: wallet) + + print(psbt.serialize()) + XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") + + let walletDidSign: Bool = try wallet.sign(psbt: psbt) + XCTAssertTrue(walletDidSign, "Wallet did not sign transaction") + + let tx: Transaction = psbt.extractTx() + print(tx.txid()) + try esploraClient.broadcast(transaction: tx) + } }