Compare commits

..

19 Commits

Author SHA1 Message Date
thunderbiscuit
d5cf483223 chore: update libraries to official alpha 11 release versions 2024-05-21 13:11:41 -04:00
thunderbiscuit
c6174199dd test: fix swift tests to print amount in sats 2024-05-21 12:06:55 -04:00
thunderbiscuit
9c45254c3e test: fix live android tests 2024-05-21 11:36:29 -04:00
thunderbiscuit
260a0a65b3 chore: bump library version to alpha 11 2024-05-21 11:35:46 -04:00
thunderbiscuit
72985f14ad tests: update python tests to use amount type 2024-05-21 10:30:48 -04:00
thunderbiscuit
5e3e24906f feat: add lock_time method on transaction type 2024-05-16 14:30:47 -04:00
thunderbiscuit
c702894143 feat: add output method on transaction type 2024-05-16 14:28:43 -04:00
thunderbiscuit
ecdd7c239b feat: add input method on transaction type 2024-05-16 14:03:28 -04:00
thunderbiscuit
ca8a3d0471 refactor: streamline blockchain clients error names 2024-05-16 10:41:04 -04:00
thunderbiscuit
8f4c80cb98 test: add electrum client test 2024-05-16 10:17:51 -04:00
thunderbiscuit
4aec4b0434 feat: add broadcast method on electrum client 2024-05-16 10:17:51 -04:00
thunderbiscuit
1913c45ef9 feat: add sync method on electrum client 2024-05-16 10:17:51 -04:00
thunderbiscuit
815fe5f62d feat: add full_scan method on electrum client 2024-05-16 10:17:50 -04:00
thunderbiscuit
8d30c86076 feat: add simple electrum client 2024-05-16 10:17:50 -04:00
thunderbiscuit
c88b33473b test: add memory wallet test 2024-05-16 10:16:58 -04:00
thunderbiscuit
79e7ab73ea feat: add memory wallet 2024-05-15 14:11:02 -04:00
Matthew
f169b1a52f chore: standard capitalization in error messages 2024-05-14 16:33:03 -05:00
Matthew
97d9bb6fbf chore: bump rust bdk to alpha 11 2024-05-14 14:44:26 -05:00
thunderbiscuit
f27bada9c9 test: better messages when tests fail for low balance 2024-05-10 12:45:56 -04:00
31 changed files with 1169 additions and 246 deletions

View File

@@ -3,6 +3,9 @@ Changelog information can also be found in each release's git tag (which can be
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0-alpha.11]
This release adds the new `Amount` type, as well as more fine-grain errors.
## [1.0.0-alpha.7]
This release brings back into the 1.0 API a number of APIs from the 0.31 release, and adds the new flat file persistence feature, as well as more fine-grain errors.

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
libraryVersion=1.0.0-alpha.10-SNAPSHOT
libraryVersion=1.0.0-alpha.11

View File

@@ -14,7 +14,7 @@ private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
@RunWith(AndroidJUnit4::class)
class LiveTxBuilderTest {
private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence3.db"
@AfterTest
fun cleanup() {
@@ -28,18 +28,20 @@ class LiveTxBuilderTest {
fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet)
@@ -51,21 +53,23 @@ class LiveTxBuilderTest {
fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
)
val psbt: Psbt = TxBuilder()

View File

@@ -14,7 +14,7 @@ private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
@RunWith(AndroidJUnit4::class)
class LiveWalletTest {
private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence2.db"
@AfterTest
fun cleanup() {
@@ -33,11 +33,13 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
val balance: Balance = wallet.getBalance()
println("Balance: $balance")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
@@ -58,17 +60,16 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) {
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(4uL))
.finish(wallet)
@@ -82,7 +83,7 @@ class LiveWalletTest {
println("Txid is: ${tx.txid()}")
val txFee: ULong = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}")
println("Tx fee is: $txFee")
val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")

View File

@@ -13,7 +13,7 @@ import kotlin.test.AfterTest
@RunWith(AndroidJUnit4::class)
class OfflineWalletTest {
private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence1.db"
@AfterTest
fun cleanup() {
@@ -72,7 +72,7 @@ class OfflineWalletTest {
assertEquals(
expected = 0uL,
actual = wallet.getBalance().total
actual = wallet.getBalance().total.toSat()
)
}
}

80
bdk-ffi/Cargo.lock generated
View File

@@ -138,9 +138,9 @@ dependencies = [
[[package]]
name = "bdk"
version = "1.0.0-alpha.10"
version = "1.0.0-alpha.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fc0ebc2a63463f709cfdfbb7e7877b9975bcaea9d2d4f02f97ad012de37e3b"
checksum = "65c23f2903ac5dbb7b35934ae319aadc946201e4fa51b652440bd1c8fa3080ee"
dependencies = [
"anyhow",
"bdk_chain",
@@ -157,10 +157,11 @@ dependencies = [
[[package]]
name = "bdk-ffi"
version = "1.0.0-alpha.10"
version = "1.0.0-alpha.11"
dependencies = [
"assert_matches",
"bdk",
"bdk_electrum",
"bdk_esplora",
"bdk_file_store",
"bitcoin-internals",
@@ -170,9 +171,9 @@ dependencies = [
[[package]]
name = "bdk_chain"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e879c03ebf3a64643295152a19a8b0e0a3af22e25539d2bc56ce07d07b059c33"
checksum = "440ec5b1c8911f126b540e05c98493b699b497a3cb90c5e9c5eee21cdd8d1e01"
dependencies = [
"bitcoin",
"miniscript",
@@ -180,10 +181,20 @@ dependencies = [
]
[[package]]
name = "bdk_esplora"
version = "0.12.0"
name = "bdk_electrum"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0aad9d99b103cd9c67ce1f4702720f2813db7aeba72abc9628ae9b00462a492"
checksum = "44bbf3b0031651a37a48bdfab0c1d96a305b587f616593d34df9b1ff63efc4ff"
dependencies = [
"bdk_chain",
"electrum-client",
]
[[package]]
name = "bdk_esplora"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb5b46f8c256bc083640342bd0d35ec1963971f18800c3fee1a9189eda60ecd"
dependencies = [
"bdk_chain",
"esplora-client",
@@ -191,9 +202,9 @@ dependencies = [
[[package]]
name = "bdk_file_store"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "492a011ee853773bce14f2d899fa34fe3ac3b5f39eeb1504d0d2b28de448bd73"
checksum = "5dfd7e9a5edb8d384ea1836b0bcd4febdd3211815acc058d64c7e284776d69ab"
dependencies = [
"anyhow",
"bdk_chain",
@@ -204,9 +215,9 @@ dependencies = [
[[package]]
name = "bdk_persist"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f7d6b38071ee828329434f86799e0bb6aaa5a4256e225480c2c53b7b2df295"
checksum = "aba103c2108dd0f0b452650043d21c449ae07ce866dbaea29a9c59899a5964f0"
dependencies = [
"anyhow",
"bdk_chain",
@@ -286,6 +297,12 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.6.0"
@@ -382,6 +399,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "electrum-client"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2"
dependencies = [
"bitcoin",
"byteorder",
"libc",
"log",
"rustls",
"serde",
"serde_json",
"webpki-roots",
"winapi",
]
[[package]]
name = "esplora-client"
version = "0.7.0"
@@ -1129,6 +1163,28 @@ dependencies = [
"nom",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk-ffi"
version = "1.0.0-alpha.10"
version = "1.0.0-alpha.11"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
edition = "2018"
@@ -18,9 +18,10 @@ path = "uniffi-bindgen.rs"
default = ["uniffi/cli"]
[dependencies]
bdk = { version = "1.0.0-alpha.10", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.12.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
bdk_file_store = { version = "0.10.0" }
bdk = { version = "1.0.0-alpha.11", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.13.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
bdk_electrum = { version = "0.13.0" }
bdk_file_store = { version = "0.11.0" }
uniffi = { version = "=0.26.1" }
bitcoin-internals = { version = "0.2.0", features = ["alloc"] }

View File

@@ -101,6 +101,27 @@ interface DescriptorKeyError {
Bip32(string error_message);
};
[Error]
interface ElectrumError {
IOError(string error_message);
Json(string error_message);
Hex(string error_message);
Protocol(string error_message);
Bitcoin(string error_message);
AlreadySubscribed();
NotSubscribed();
InvalidResponse(string error_message);
Message(string error_message);
InvalidDNSNameError(string domain);
MissingDomain();
AllAttemptsErrored();
SharedIOError(string error_message);
CouldntLockReader();
Mpsc();
CouldNotCreateConnection(string error_message);
RequestAlreadyConsumed();
};
[Error]
interface EsploraError {
Minreq(string error_message);
@@ -131,6 +152,19 @@ enum FeeRateError {
"ArithmeticOverflow"
};
[Error]
interface ParseAmountError {
Negative();
TooBig();
TooPrecise();
InvalidFormat();
InputTooLarge();
InvalidCharacter(string error_message);
UnknownDenomination(string error_message);
PossiblyConfusingDenomination(string error_message);
OtherParseAmountErr();
};
[Error]
interface PersistenceError {
Write(string error_message);
@@ -185,6 +219,7 @@ interface WalletCreationError {
NotInitialized();
LoadedGenesisDoesNotMatch(string expected, string got);
LoadedNetworkDoesNotMatch(Network expected, Network? got);
LoadedDescriptorDoesNotMatch(string got, KeychainKind keychain);
};
// ------------------------------------------------------------------------
@@ -203,17 +238,17 @@ dictionary AddressInfo {
};
dictionary Balance {
u64 immature;
Amount immature;
u64 trusted_pending;
Amount trusted_pending;
u64 untrusted_pending;
Amount untrusted_pending;
u64 confirmed;
Amount confirmed;
u64 trusted_spendable;
Amount trusted_spendable;
u64 total;
Amount total;
};
dictionary LocalOutput {
@@ -257,6 +292,9 @@ interface Wallet {
[Throws=WalletCreationError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network);
[Name=new_no_persist, Throws=DescriptorError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, Network network);
[Throws=PersistenceError]
AddressInfo reveal_next_address(KeychainKind keychain);
@@ -302,7 +340,7 @@ interface Update {};
interface TxBuilder {
constructor();
TxBuilder add_recipient([ByRef] Script script, u64 amount);
TxBuilder add_recipient([ByRef] Script script, Amount amount);
TxBuilder set_recipients(sequence<ScriptAmount> recipients);
@@ -450,18 +488,36 @@ interface EsploraClient {
void broadcast([ByRef] Transaction transaction);
};
// ------------------------------------------------------------------------
// bdk_electrum crate
// ------------------------------------------------------------------------
interface ElectrumClient {
[Throws=ElectrumError]
constructor(string url);
[Throws=ElectrumError]
Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 batch_size, boolean fetch_prev_txouts);
[Throws=ElectrumError]
Update sync(SyncRequest sync_request, u64 batch_size, boolean fetch_prev_txouts);
[Throws=ElectrumError]
string broadcast([ByRef] Transaction transaction);
};
// ------------------------------------------------------------------------
// bdk-ffi-defined types
// ------------------------------------------------------------------------
dictionary ScriptAmount {
Script script;
u64 amount;
Amount amount;
};
dictionary SentAndReceivedValues {
u64 sent;
u64 received;
Amount sent;
Amount received;
};
// ------------------------------------------------------------------------
@@ -526,6 +582,12 @@ interface Transaction {
sequence<u8> serialize();
u64 weight();
sequence<TxIn> input();
sequence<TxOut> output();
u32 lock_time();
};
interface Psbt {
@@ -543,6 +605,18 @@ dictionary OutPoint {
u32 vout;
};
interface Amount {
[Name=from_sat]
constructor(u64 from_sat);
[Name=from_btc, Throws=ParseAmountError]
constructor(f64 from_btc);
u64 to_sat();
f64 to_btc();
};
interface FeeRate {
[Name=from_sat_per_vb, Throws=FeeRateError]
constructor(u64 sat_per_vb);
@@ -556,3 +630,10 @@ interface FeeRate {
u64 to_sat_per_kwu();
};
dictionary TxIn {
OutPoint previous_output;
Script script_sig;
u32 sequence;
sequence<sequence<u8>> witness;
};

View File

@@ -1,23 +1,60 @@
use crate::error::{AddressError, FeeRateError, PsbtParseError, TransactionError};
use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked};
use bdk::bitcoin::amount::ParseAmountError;
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut;
use bdk::bitcoin::consensus::encode::serialize;
use bdk::bitcoin::consensus::Decodable;
use bdk::bitcoin::psbt::ExtractTxError;
use bdk::bitcoin::Address as BdkAddress;
use bdk::bitcoin::Amount as BdkAmount;
use bdk::bitcoin::FeeRate as BdkFeeRate;
use bdk::bitcoin::Network;
use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::bitcoin::TxIn as BdkTxIn;
use bdk::bitcoin::Txid;
use std::io::Cursor;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Amount(pub(crate) BdkAmount);
impl Amount {
pub fn from_sat(sat: u64) -> Self {
Amount(BdkAmount::from_sat(sat))
}
pub fn from_btc(btc: f64) -> Result<Self, ParseAmountError> {
let bdk_amount = BdkAmount::from_btc(btc).map_err(ParseAmountError::from)?;
Ok(Amount(bdk_amount))
}
pub fn to_sat(&self) -> u64 {
self.0.to_sat()
}
pub fn to_btc(&self) -> f64 {
self.0.to_btc()
}
}
impl From<Amount> for BdkAmount {
fn from(amount: Amount) -> Self {
amount.0
}
}
impl From<BdkAmount> for Amount {
fn from(amount: BdkAmount) -> Self {
Amount(amount)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Script(pub(crate) BdkScriptBuf);
@@ -132,6 +169,18 @@ impl Transaction {
pub fn serialize(&self) -> Vec<u8> {
serialize(&self.0)
}
pub fn input(&self) -> Vec<TxIn> {
self.0.input.iter().map(|tx_in| tx_in.into()).collect()
}
pub fn output(&self) -> Vec<TxOut> {
self.0.output.iter().map(|tx_out| tx_out.into()).collect()
}
pub fn lock_time(&self) -> u32 {
self.0.lock_time.to_consensus_u32()
}
}
impl From<BdkTransaction> for Transaction {
@@ -202,6 +251,28 @@ impl From<&BdkOutPoint> for OutPoint {
}
}
#[derive(Debug, Clone)]
pub struct TxIn {
pub previous_output: OutPoint,
pub script_sig: Arc<Script>,
pub sequence: u32,
pub witness: Vec<Vec<u8>>,
}
impl From<&BdkTxIn> for TxIn {
fn from(tx_in: &BdkTxIn) -> Self {
TxIn {
previous_output: OutPoint {
txid: tx_in.previous_output.txid.to_string(),
vout: tx_in.previous_output.vout,
},
script_sig: Arc::new(Script(tx_in.script_sig.clone())),
sequence: tx_in.sequence.0,
witness: tx_in.witness.to_vec(),
}
}
}
#[derive(Debug, Clone)]
pub struct TxOut {
pub value: u64,

95
bdk-ffi/src/electrum.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::bitcoin::Transaction;
use crate::error::ElectrumError;
use crate::types::{FullScanRequest, SyncRequest};
use crate::wallet::Update;
use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk::chain::spk_client::FullScanResult as BdkFullScanResult;
use bdk::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk::chain::spk_client::SyncResult as BdkSyncResult;
use bdk::KeychainKind;
use bdk_electrum::electrum_client::{Client as BdkBlockingClient, ElectrumApi};
use bdk_electrum::{ElectrumExt, ElectrumFullScanResult, ElectrumSyncResult};
use std::collections::BTreeMap;
use std::sync::Arc;
pub struct ElectrumClient(BdkBlockingClient);
impl ElectrumClient {
pub fn new(url: String) -> Result<Self, ElectrumError> {
let client = BdkBlockingClient::new(url.as_str())?;
Ok(Self(client))
}
pub fn full_scan(
&self,
request: Arc<FullScanRequest>,
stop_gap: u64,
batch_size: u64,
fetch_prev_txouts: bool,
) -> Result<Arc<Update>, ElectrumError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkFullScanRequest<KeychainKind> = request
.0
.lock()
.unwrap()
.take()
.ok_or(ElectrumError::RequestAlreadyConsumed)?;
let electrum_result: ElectrumFullScanResult<KeychainKind> = self.0.full_scan(
request,
stop_gap as usize,
batch_size as usize,
fetch_prev_txouts,
)?;
let full_scan_result: BdkFullScanResult<KeychainKind> =
electrum_result.with_confirmation_time_height_anchor(&self.0)?;
let update = bdk::wallet::Update {
last_active_indices: full_scan_result.last_active_indices,
graph: full_scan_result.graph_update,
chain: Some(full_scan_result.chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn sync(
&self,
request: Arc<SyncRequest>,
batch_size: u64,
fetch_prev_txouts: bool,
) -> Result<Arc<Update>, ElectrumError> {
// using option and take is not ideal but the only way to take full ownership of the request
let request: BdkSyncRequest = request
.0
.lock()
.unwrap()
.take()
.ok_or(ElectrumError::RequestAlreadyConsumed)?;
let electrum_result: ElectrumSyncResult =
self.0
.sync(request, batch_size as usize, fetch_prev_txouts)?;
let sync_result: BdkSyncResult =
electrum_result.with_confirmation_time_height_anchor(&self.0)?;
let update = bdk::wallet::Update {
last_active_indices: BTreeMap::default(),
graph: sync_result.graph_update,
chain: Some(sync_result.chain_update),
};
Ok(Arc::new(Update(update)))
}
pub fn broadcast(&self, transaction: &Transaction) -> Result<String, ElectrumError> {
let bdk_transaction: BdkTransaction = transaction.into();
self.0
.transaction_broadcast(&bdk_transaction)
.map_err(ElectrumError::from)
.map(|txid| txid.to_string())
}
}

View File

@@ -13,16 +13,20 @@ use bdk::wallet::error::CreateTxError as BdkCreateTxError;
use bdk::wallet::signer::SignerError as BdkSignerError;
use bdk::wallet::tx_builder::AddUtxoError;
use bdk::wallet::NewOrLoadError;
use bdk_electrum::electrum_client::Error as BdkElectrumError;
use bdk_esplora::esplora_client::{Error as BdkEsploraError, Error};
use bdk_file_store::FileError as BdkFileError;
use bitcoin_internals::hex::display::DisplayHex;
use bdk::bitcoin::amount::ParseAmountError as BdkParseAmountError;
use std::convert::TryInto;
use bdk::bitcoin::address::Error as BdkAddressError;
use bdk::bitcoin::consensus::encode::Error as BdkEncodeError;
use bdk::bitcoin::psbt::ExtractTxError as BdkExtractTxError;
use bdk::chain::local_chain::CannotConnectError as BdkCannotConnectError;
use bdk::KeychainKind;
// ------------------------------------------------------------------------
// error definitions
@@ -65,37 +69,37 @@ pub enum AddressError {
#[derive(Debug, thiserror::Error)]
pub enum Bip32Error {
#[error("Cannot derive from a hardened key")]
#[error("cannot derive from a hardened key")]
CannotDeriveFromHardenedKey,
#[error("Secp256k1 error: {error_message}")]
#[error("secp256k1 error: {error_message}")]
Secp256k1 { error_message: String },
#[error("Invalid child number: {child_number}")]
#[error("invalid child number: {child_number}")]
InvalidChildNumber { child_number: u32 },
#[error("Invalid format for child number")]
#[error("invalid format for child number")]
InvalidChildNumberFormat,
#[error("Invalid derivation path format")]
#[error("invalid derivation path format")]
InvalidDerivationPathFormat,
#[error("Unknown version: {version}")]
#[error("unknown version: {version}")]
UnknownVersion { version: String },
#[error("Wrong extended key length: {length}")]
#[error("wrong extended key length: {length}")]
WrongExtendedKeyLength { length: u32 },
#[error("Base58 error: {error_message}")]
#[error("base58 error: {error_message}")]
Base58 { error_message: String },
#[error("Hexadecimal conversion error: {error_message}")]
#[error("hexadecimal conversion error: {error_message}")]
Hex { error_message: String },
#[error("Invalid public key hex length: {length}")]
#[error("invalid public key hex length: {length}")]
InvalidPublicKeyHexLength { length: u32 },
#[error("Unknown error: {error_message}")]
#[error("unknown error: {error_message}")]
UnknownError { error_message: String },
}
@@ -134,70 +138,70 @@ pub enum CannotConnectError {
#[derive(Debug, thiserror::Error)]
pub enum CreateTxError {
#[error("Descriptor error: {error_message}")]
#[error("descriptor error: {error_message}")]
Descriptor { error_message: String },
#[error("Persistence failure: {error_message}")]
#[error("persistence failure: {error_message}")]
Persist { error_message: String },
#[error("Policy error: {error_message}")]
#[error("policy error: {error_message}")]
Policy { error_message: String },
#[error("Spending policy required for {kind}")]
#[error("spending policy required for {kind}")]
SpendingPolicyRequired { kind: String },
#[error("Unsupported version 0")]
#[error("unsupported version 0")]
Version0,
#[error("Unsupported version 1 with CSV")]
#[error("unsupported version 1 with csv")]
Version1Csv,
#[error("Lock time conflict: requested {requested}, but required {required}")]
#[error("lock time conflict: requested {requested}, but required {required}")]
LockTime { requested: String, required: String },
#[error("Transaction requires RBF sequence number")]
#[error("transaction requires rbf sequence number")]
RbfSequence,
#[error("RBF sequence: {rbf}, CSV sequence: {csv}")]
#[error("rbf sequence: {rbf}, csv sequence: {csv}")]
RbfSequenceCsv { rbf: String, csv: String },
#[error("Fee too low: {required} sat required")]
#[error("fee too low: {required} sat required")]
FeeTooLow { required: u64 },
#[error("Fee rate too low: {required}")]
#[error("fee rate too low: {required}")]
FeeRateTooLow { required: String },
#[error("No UTXOs selected for the transaction")]
#[error("no utxos selected for the transaction")]
NoUtxosSelected,
#[error("Output value below dust limit at index {index}")]
#[error("output value below dust limit at index {index}")]
OutputBelowDustLimit { index: u64 },
#[error("Change policy descriptor error")]
#[error("change policy descriptor error")]
ChangePolicyDescriptor,
#[error("Coin selection failed: {error_message}")]
#[error("coin selection failed: {error_message}")]
CoinSelection { error_message: String },
#[error("Insufficient funds: needed {needed} sat, available {available} sat")]
#[error("insufficient funds: needed {needed} sat, available {available} sat")]
InsufficientFunds { needed: u64, available: u64 },
#[error("Transaction has no recipients")]
#[error("transaction has no recipients")]
NoRecipients,
#[error("PSBT creation error: {error_message}")]
#[error("psbt creation error: {error_message}")]
Psbt { error_message: String },
#[error("Missing key origin for: {key}")]
#[error("missing key origin for: {key}")]
MissingKeyOrigin { key: String },
#[error("Reference to an unknown UTXO: {outpoint}")]
#[error("reference to an unknown utxo: {outpoint}")]
UnknownUtxo { outpoint: String },
#[error("Missing non-witness UTXO for outpoint: {outpoint}")]
#[error("missing non-witness utxo for outpoint: {outpoint}")]
MissingNonWitnessUtxo { outpoint: String },
#[error("Miniscript PSBT error: {error_message}")]
#[error("miniscript psbt error: {error_message}")]
MiniscriptPsbt { error_message: String },
}
@@ -224,19 +228,19 @@ pub enum DescriptorError {
#[error("invalid descriptor character: {char}")]
InvalidDescriptorCharacter { char: String },
#[error("BIP32 error: {error_message}")]
#[error("bip32 error: {error_message}")]
Bip32 { error_message: String },
#[error("Base58 error: {error_message}")]
#[error("base58 error: {error_message}")]
Base58 { error_message: String },
#[error("Key-related error: {error_message}")]
#[error("key-related error: {error_message}")]
Pk { error_message: String },
#[error("Miniscript error: {error_message}")]
#[error("miniscript error: {error_message}")]
Miniscript { error_message: String },
#[error("Hex decoding error: {error_message}")]
#[error("hex decoding error: {error_message}")]
Hex { error_message: String },
}
@@ -252,6 +256,60 @@ pub enum DescriptorKeyError {
Bip32 { error_message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum ElectrumError {
#[error("{error_message}")]
IOError { error_message: String },
#[error("{error_message}")]
Json { error_message: String },
#[error("{error_message}")]
Hex { error_message: String },
#[error("electrum server error: {error_message}")]
Protocol { error_message: String },
#[error("{error_message}")]
Bitcoin { error_message: String },
#[error("already subscribed to the notifications of an address")]
AlreadySubscribed,
#[error("not subscribed to the notifications of an address")]
NotSubscribed,
#[error("error during the deserialization of a response from the server: {error_message}")]
InvalidResponse { error_message: String },
#[error("{error_message}")]
Message { error_message: String },
#[error("invalid domain name {domain} not matching SSL certificate")]
InvalidDNSNameError { domain: String },
#[error("missing domain while it was explicitly asked to validate it")]
MissingDomain,
#[error("made one or multiple attempts, all errored")]
AllAttemptsErrored,
#[error("{error_message}")]
SharedIOError { error_message: String },
#[error("couldn't take a lock on the reader mutex. This means that there's already another reader thread is running")]
CouldntLockReader,
#[error("broken IPC communication channel: the other thread probably has exited")]
Mpsc,
#[error("{error_message}")]
CouldNotCreateConnection { error_message: String },
#[error("the request has already been consumed")]
RequestAlreadyConsumed,
}
#[derive(Debug, thiserror::Error)]
pub enum EsploraError {
#[error("minreq error: {error_message}")]
@@ -263,7 +321,7 @@ pub enum EsploraError {
#[error("parsing error: {error_message}")]
Parsing { error_message: String },
#[error("Invalid status code, unable to convert to u16: {error_message}")]
#[error("invalid status code, unable to convert to u16: {error_message}")]
StatusCode { error_message: String },
#[error("bitcoin encoding error: {error_message}")]
@@ -284,10 +342,10 @@ pub enum EsploraError {
#[error("header hash not found")]
HeaderHashNotFound,
#[error("invalid HTTP header name: {name}")]
#[error("invalid http header name: {name}")]
InvalidHttpHeaderName { name: String },
#[error("invalid HTTP header value: {value}")]
#[error("invalid http header value: {value}")]
InvalidHttpHeaderValue { value: String },
#[error("the request has already been consumed")]
@@ -317,6 +375,37 @@ pub enum FeeRateError {
ArithmeticOverflow,
}
#[derive(Debug, thiserror::Error)]
pub enum ParseAmountError {
#[error("amount is negative")]
Negative,
#[error("amount is too large")]
TooBig,
#[error("amount is too precise")]
TooPrecise,
#[error("invalid amount format")]
InvalidFormat,
#[error("input is too large")]
InputTooLarge,
#[error("invalid character: {error_message}")]
InvalidCharacter { error_message: String },
#[error("unknown denomination: {error_message}")]
UnknownDenomination { error_message: String },
#[error("possibly confusing denomination: {error_message}")]
PossiblyConfusingDenomination { error_message: String },
// Has to handle non-exhaustive
#[error("unknown parse amount error")]
OtherParseAmountErr,
}
#[derive(Debug, thiserror::Error)]
pub enum PersistenceError {
#[error("writing to persistence error: {error_message}")]
@@ -325,61 +414,61 @@ pub enum PersistenceError {
#[derive(Debug, thiserror::Error)]
pub enum PsbtParseError {
#[error("error in internal PSBT data structure: {error_message}")]
#[error("error in internal psbt data structure: {error_message}")]
PsbtEncoding { error_message: String },
#[error("error in PSBT base64 encoding: {error_message}")]
#[error("error in psbt base64 encoding: {error_message}")]
Base64Encoding { error_message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum SignerError {
#[error("Missing key for signing")]
#[error("missing key for signing")]
MissingKey,
#[error("Invalid key provided")]
#[error("invalid key provided")]
InvalidKey,
#[error("User canceled operation")]
#[error("user canceled operation")]
UserCanceled,
#[error("Input index out of range")]
#[error("input index out of range")]
InputIndexOutOfRange,
#[error("Missing non-witness UTXO information")]
#[error("missing non-witness utxo information")]
MissingNonWitnessUtxo,
#[error("Invalid non-witness UTXO information provided")]
#[error("invalid non-witness utxo information provided")]
InvalidNonWitnessUtxo,
#[error("Missing witness UTXO")]
#[error("missing witness utxo")]
MissingWitnessUtxo,
#[error("Missing witness script")]
#[error("missing witness script")]
MissingWitnessScript,
#[error("Missing HD keypath")]
#[error("missing hd keypath")]
MissingHdKeypath,
#[error("Non-standard sighash type used")]
#[error("non-standard sighash type used")]
NonStandardSighash,
#[error("Invalid sighash type provided")]
#[error("invalid sighash type provided")]
InvalidSighash,
#[error("Error with sighash computation: {error_message}")]
#[error("error with sighash computation: {error_message}")]
SighashError { error_message: String },
#[error("Miniscript Psbt error: {error_message}")]
#[error("miniscript psbt error: {error_message}")]
MiniscriptPsbt { error_message: String },
#[error("External error: {error_message}")]
#[error("external error: {error_message}")]
External { error_message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum TransactionError {
#[error("IO error")]
#[error("io error")]
Io,
#[error("allocation of oversized vector")]
@@ -388,7 +477,7 @@ pub enum TransactionError {
#[error("invalid checksum: expected={expected} actual={actual}")]
InvalidChecksum { expected: String, actual: String },
#[error("non-minimal varint")]
#[error("non-minimal var int")]
NonMinimalVarInt,
#[error("parse failed")]
@@ -434,6 +523,9 @@ pub enum WalletCreationError {
expected: Network,
got: Option<Network>,
},
#[error("loaded descriptor '{got}' does not match what was provided '{keychain:?}'")]
LoadedDescriptorDoesNotMatch { got: String, keychain: KeychainKind },
}
// ------------------------------------------------------------------------
@@ -466,6 +558,51 @@ impl From<BdkAddressError> for AddressError {
}
}
impl From<BdkElectrumError> for ElectrumError {
fn from(error: BdkElectrumError) -> Self {
match error {
BdkElectrumError::IOError(e) => ElectrumError::IOError {
error_message: e.to_string(),
},
BdkElectrumError::JSON(e) => ElectrumError::Json {
error_message: e.to_string(),
},
BdkElectrumError::Hex(e) => ElectrumError::Hex {
error_message: e.to_string(),
},
BdkElectrumError::Protocol(e) => ElectrumError::Protocol {
error_message: e.to_string(),
},
BdkElectrumError::Bitcoin(e) => ElectrumError::Bitcoin {
error_message: e.to_string(),
},
BdkElectrumError::AlreadySubscribed(_) => ElectrumError::AlreadySubscribed,
BdkElectrumError::NotSubscribed(_) => ElectrumError::NotSubscribed,
BdkElectrumError::InvalidResponse(e) => ElectrumError::InvalidResponse {
error_message: e.to_string(),
},
BdkElectrumError::Message(e) => ElectrumError::Message {
error_message: e.to_string(),
},
BdkElectrumError::InvalidDNSNameError(domain) => {
ElectrumError::InvalidDNSNameError { domain }
}
BdkElectrumError::MissingDomain => ElectrumError::MissingDomain,
BdkElectrumError::AllAttemptsErrored(_) => ElectrumError::AllAttemptsErrored,
BdkElectrumError::SharedIOError(e) => ElectrumError::SharedIOError {
error_message: e.to_string(),
},
BdkElectrumError::CouldntLockReader => ElectrumError::CouldntLockReader,
BdkElectrumError::Mpsc => ElectrumError::Mpsc,
BdkElectrumError::CouldNotCreateConnection(error_message) => {
ElectrumError::CouldNotCreateConnection {
error_message: error_message.to_string(),
}
}
}
}
}
impl From<ParseError> for AddressError {
fn from(error: ParseError) -> Self {
match error {
@@ -800,6 +937,28 @@ impl From<BdkExtractTxError> for ExtractTxError {
}
}
impl From<BdkParseAmountError> for ParseAmountError {
fn from(error: BdkParseAmountError) -> Self {
match error {
BdkParseAmountError::Negative => ParseAmountError::Negative,
BdkParseAmountError::TooBig => ParseAmountError::TooBig,
BdkParseAmountError::InvalidFormat => ParseAmountError::InvalidFormat,
BdkParseAmountError::TooPrecise => ParseAmountError::TooPrecise,
BdkParseAmountError::InputTooLarge => ParseAmountError::InputTooLarge,
BdkParseAmountError::InvalidCharacter(c) => ParseAmountError::InvalidCharacter {
error_message: c.to_string(),
},
BdkParseAmountError::UnknownDenomination(s) => {
ParseAmountError::UnknownDenomination { error_message: s }
}
BdkParseAmountError::PossiblyConfusingDenomination(s) => {
ParseAmountError::PossiblyConfusingDenomination { error_message: s }
}
_ => ParseAmountError::OtherParseAmountErr,
}
}
}
impl From<std::io::Error> for PersistenceError {
fn from(error: std::io::Error) -> Self {
PersistenceError::Write {
@@ -902,6 +1061,12 @@ impl From<NewOrLoadError> for WalletCreationError {
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
WalletCreationError::LoadedNetworkDoesNotMatch { expected, got }
}
NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
WalletCreationError::LoadedDescriptorDoesNotMatch {
got: format!("{:?}", got),
keychain,
}
}
}
}
}
@@ -914,13 +1079,15 @@ impl From<NewOrLoadError> for WalletCreationError {
mod test {
use crate::error::{
AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError,
DescriptorKeyError, EsploraError, ExtractTxError, FeeRateError, PersistenceError,
PsbtParseError, TransactionError, TxidParseError, WalletCreationError,
DescriptorKeyError, ElectrumError, EsploraError, ExtractTxError, FeeRateError,
ParseAmountError, PersistenceError, PsbtParseError, TransactionError, TxidParseError,
WalletCreationError,
};
use crate::CalculateFeeError;
use crate::OutPoint;
use crate::SignerError;
use bdk::bitcoin::Network;
use bdk::KeychainKind;
#[test]
fn test_error_address() {
@@ -972,57 +1139,57 @@ mod test {
let cases = vec![
(
Bip32Error::CannotDeriveFromHardenedKey,
"Cannot derive from a hardened key",
"cannot derive from a hardened key",
),
(
Bip32Error::Secp256k1 {
error_message: "failure".to_string(),
},
"Secp256k1 error: failure",
"secp256k1 error: failure",
),
(
Bip32Error::InvalidChildNumber { child_number: 123 },
"Invalid child number: 123",
"invalid child number: 123",
),
(
Bip32Error::InvalidChildNumberFormat,
"Invalid format for child number",
"invalid format for child number",
),
(
Bip32Error::InvalidDerivationPathFormat,
"Invalid derivation path format",
"invalid derivation path format",
),
(
Bip32Error::UnknownVersion {
version: "0x123".to_string(),
},
"Unknown version: 0x123",
"unknown version: 0x123",
),
(
Bip32Error::WrongExtendedKeyLength { length: 512 },
"Wrong extended key length: 512",
"wrong extended key length: 512",
),
(
Bip32Error::Base58 {
error_message: "error".to_string(),
},
"Base58 error: error",
"base58 error: error",
),
(
Bip32Error::Hex {
error_message: "error".to_string(),
},
"Hexadecimal conversion error: error",
"hexadecimal conversion error: error",
),
(
Bip32Error::InvalidPublicKeyHexLength { length: 66 },
"Invalid public key hex length: 66",
"invalid public key hex length: 66",
),
(
Bip32Error::UnknownError {
error_message: "mystery".to_string(),
},
"Unknown error: mystery",
"unknown error: mystery",
),
];
@@ -1110,111 +1277,111 @@ mod test {
CreateTxError::Descriptor {
error_message: "Descriptor failure".to_string(),
},
"Descriptor error: Descriptor failure",
"descriptor error: Descriptor failure",
),
(
CreateTxError::Persist {
error_message: "Persistence error".to_string(),
},
"Persistence failure: Persistence error",
"persistence failure: Persistence error",
),
(
CreateTxError::Policy {
error_message: "Policy violation".to_string(),
},
"Policy error: Policy violation",
"policy error: Policy violation",
),
(
CreateTxError::SpendingPolicyRequired {
kind: "multisig".to_string(),
},
"Spending policy required for multisig",
"spending policy required for multisig",
),
(CreateTxError::Version0, "Unsupported version 0"),
(CreateTxError::Version1Csv, "Unsupported version 1 with CSV"),
(CreateTxError::Version0, "unsupported version 0"),
(CreateTxError::Version1Csv, "unsupported version 1 with csv"),
(
CreateTxError::LockTime {
requested: "today".to_string(),
required: "tomorrow".to_string(),
},
"Lock time conflict: requested today, but required tomorrow",
"lock time conflict: requested today, but required tomorrow",
),
(
CreateTxError::RbfSequence,
"Transaction requires RBF sequence number",
"transaction requires rbf sequence number",
),
(
CreateTxError::RbfSequenceCsv {
rbf: "123".to_string(),
csv: "456".to_string(),
},
"RBF sequence: 123, CSV sequence: 456",
"rbf sequence: 123, csv sequence: 456",
),
(
CreateTxError::FeeTooLow { required: 1000 },
"Fee too low: 1000 sat required",
"fee too low: 1000 sat required",
),
(
CreateTxError::FeeRateTooLow {
required: "5 sat/vB".to_string(),
},
"Fee rate too low: 5 sat/vB",
"fee rate too low: 5 sat/vB",
),
(
CreateTxError::NoUtxosSelected,
"No UTXOs selected for the transaction",
"no utxos selected for the transaction",
),
(
CreateTxError::OutputBelowDustLimit { index: 2 },
"Output value below dust limit at index 2",
"output value below dust limit at index 2",
),
(
CreateTxError::ChangePolicyDescriptor,
"Change policy descriptor error",
"change policy descriptor error",
),
(
CreateTxError::CoinSelection {
error_message: "No suitable outputs".to_string(),
},
"Coin selection failed: No suitable outputs",
"coin selection failed: No suitable outputs",
),
(
CreateTxError::InsufficientFunds {
needed: 5000,
available: 3000,
},
"Insufficient funds: needed 5000 sat, available 3000 sat",
"insufficient funds: needed 5000 sat, available 3000 sat",
),
(CreateTxError::NoRecipients, "Transaction has no recipients"),
(CreateTxError::NoRecipients, "transaction has no recipients"),
(
CreateTxError::Psbt {
error_message: "PSBT creation failed".to_string(),
},
"PSBT creation error: PSBT creation failed",
"psbt creation error: PSBT creation failed",
),
(
CreateTxError::MissingKeyOrigin {
key: "xpub...".to_string(),
},
"Missing key origin for: xpub...",
"missing key origin for: xpub...",
),
(
CreateTxError::UnknownUtxo {
outpoint: "outpoint123".to_string(),
},
"Reference to an unknown UTXO: outpoint123",
"reference to an unknown utxo: outpoint123",
),
(
CreateTxError::MissingNonWitnessUtxo {
outpoint: "outpoint456".to_string(),
},
"Missing non-witness UTXO for outpoint: outpoint456",
"missing non-witness utxo for outpoint: outpoint456",
),
(
CreateTxError::MiniscriptPsbt {
error_message: "Miniscript error".to_string(),
},
"Miniscript PSBT error: Miniscript error",
"miniscript psbt error: Miniscript error",
),
];
@@ -1261,31 +1428,31 @@ mod test {
DescriptorError::Bip32 {
error_message: "Bip32 error".to_string(),
},
"BIP32 error: Bip32 error",
"bip32 error: Bip32 error",
),
(
DescriptorError::Base58 {
error_message: "Base58 decode error".to_string(),
},
"Base58 error: Base58 decode error",
"base58 error: Base58 decode error",
),
(
DescriptorError::Pk {
error_message: "Public key error".to_string(),
},
"Key-related error: Public key error",
"key-related error: Public key error",
),
(
DescriptorError::Miniscript {
error_message: "Miniscript evaluation error".to_string(),
},
"Miniscript error: Miniscript evaluation error",
"miniscript error: Miniscript evaluation error",
),
(
DescriptorError::Hex {
error_message: "Hexadecimal decoding error".to_string(),
},
"Hex decoding error: Hexadecimal decoding error",
"hex decoding error: Hexadecimal decoding error",
),
];
@@ -1317,6 +1484,92 @@ mod test {
}
}
#[test]
fn test_error_electrum_client() {
let cases = vec![
(
ElectrumError::IOError { error_message: "message".to_string(), },
"message",
),
(
ElectrumError::Json { error_message: "message".to_string(), },
"message",
),
(
ElectrumError::Hex { error_message: "message".to_string(), },
"message",
),
(
ElectrumError::Protocol { error_message: "message".to_string(), },
"electrum server error: message",
),
(
ElectrumError::Bitcoin {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumError::AlreadySubscribed,
"already subscribed to the notifications of an address",
),
(
ElectrumError::NotSubscribed,
"not subscribed to the notifications of an address",
),
(
ElectrumError::InvalidResponse {
error_message: "message".to_string(),
},
"error during the deserialization of a response from the server: message",
),
(
ElectrumError::Message {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumError::InvalidDNSNameError {
domain: "domain".to_string(),
},
"invalid domain name domain not matching SSL certificate",
),
(
ElectrumError::MissingDomain,
"missing domain while it was explicitly asked to validate it",
),
(
ElectrumError::AllAttemptsErrored,
"made one or multiple attempts, all errored",
),
(
ElectrumError::SharedIOError {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumError::CouldntLockReader,
"couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"
),
(
ElectrumError::Mpsc,
"broken IPC communication channel: the other thread probably has exited",
),
(
ElectrumError::CouldNotCreateConnection {
error_message: "message".to_string(),
},
"message",
)
];
for (error, expected_message) in cases {
assert_eq!(error.to_string(), expected_message);
}
}
#[test]
fn test_error_esplora() {
let cases = vec![
@@ -1337,7 +1590,7 @@ mod test {
EsploraError::StatusCode {
error_message: "code 1234567".to_string(),
},
"Invalid status code, unable to convert to u16: code 1234567",
"invalid status code, unable to convert to u16: code 1234567",
),
(
EsploraError::Parsing {
@@ -1418,6 +1671,43 @@ mod test {
}
}
#[test]
fn test_error_parse_amount() {
let cases = vec![
(ParseAmountError::Negative, "amount is negative"),
(ParseAmountError::TooBig, "amount is too large"),
(ParseAmountError::TooPrecise, "amount is too precise"),
(ParseAmountError::InvalidFormat, "invalid amount format"),
(ParseAmountError::InputTooLarge, "input is too large"),
(
ParseAmountError::InvalidCharacter {
error_message: "invalid char".to_string(),
},
"invalid character: invalid char",
),
(
ParseAmountError::UnknownDenomination {
error_message: "unknown denom".to_string(),
},
"unknown denomination: unknown denom",
),
(
ParseAmountError::PossiblyConfusingDenomination {
error_message: "confusing denom".to_string(),
},
"possibly confusing denomination: confusing denom",
),
(
ParseAmountError::OtherParseAmountErr,
"unknown parse amount error",
),
];
for (error, expected_message) in cases {
assert_eq!(error.to_string(), expected_message);
}
}
#[test]
fn test_persistence_error() {
let cases = vec![
@@ -1449,13 +1739,13 @@ mod test {
PsbtParseError::PsbtEncoding {
error_message: "invalid PSBT structure".to_string(),
},
"error in internal PSBT data structure: invalid PSBT structure",
"error in internal psbt data structure: invalid PSBT structure",
),
(
PsbtParseError::Base64Encoding {
error_message: "base64 decode error".to_string(),
},
"error in PSBT base64 encoding: base64 decode error",
"error in psbt base64 encoding: base64 decode error",
),
];
@@ -1467,46 +1757,46 @@ mod test {
#[test]
fn test_signer_errors() {
let errors = vec![
(SignerError::MissingKey, "Missing key for signing"),
(SignerError::InvalidKey, "Invalid key provided"),
(SignerError::UserCanceled, "User canceled operation"),
(SignerError::MissingKey, "missing key for signing"),
(SignerError::InvalidKey, "invalid key provided"),
(SignerError::UserCanceled, "user canceled operation"),
(
SignerError::InputIndexOutOfRange,
"Input index out of range",
"input index out of range",
),
(
SignerError::MissingNonWitnessUtxo,
"Missing non-witness UTXO information",
"missing non-witness utxo information",
),
(
SignerError::InvalidNonWitnessUtxo,
"Invalid non-witness UTXO information provided",
"invalid non-witness utxo information provided",
),
(SignerError::MissingWitnessUtxo, "Missing witness UTXO"),
(SignerError::MissingWitnessScript, "Missing witness script"),
(SignerError::MissingHdKeypath, "Missing HD keypath"),
(SignerError::MissingWitnessUtxo, "missing witness utxo"),
(SignerError::MissingWitnessScript, "missing witness script"),
(SignerError::MissingHdKeypath, "missing hd keypath"),
(
SignerError::NonStandardSighash,
"Non-standard sighash type used",
"non-standard sighash type used",
),
(SignerError::InvalidSighash, "Invalid sighash type provided"),
(SignerError::InvalidSighash, "invalid sighash type provided"),
(
SignerError::SighashError {
error_message: "dummy error".into(),
},
"Error with sighash computation: dummy error",
"error with sighash computation: dummy error",
),
(
SignerError::MiniscriptPsbt {
error_message: "psbt issue".into(),
},
"Miniscript Psbt error: psbt issue",
"miniscript psbt error: psbt issue",
),
(
SignerError::External {
error_message: "external error".into(),
},
"External error: external error",
"external error: external error",
),
];
@@ -1518,7 +1808,7 @@ mod test {
#[test]
fn test_error_transaction() {
let cases = vec![
(TransactionError::Io, "IO error"),
(TransactionError::Io, "io error"),
(
TransactionError::OversizedVectorAllocation,
"allocation of oversized vector",
@@ -1530,7 +1820,7 @@ mod test {
},
"invalid checksum: expected=deadbeef actual=beadbeef",
),
(TransactionError::NonMinimalVarInt, "non-minimal varint"),
(TransactionError::NonMinimalVarInt, "non-minimal var int"),
(TransactionError::ParseFailed, "parse failed"),
(
TransactionError::UnsupportedSegwitFlag { flag: 1 },
@@ -1605,6 +1895,13 @@ mod test {
},
"loaded network type is not bitcoin, got Some(Testnet)".to_string(),
),
(
WalletCreationError::LoadedDescriptorDoesNotMatch {
got: "def".to_string(),
keychain: KeychainKind::External,
},
"loaded descriptor 'def' does not match what was provided 'External'".to_string(),
),
];
for (error, expected) in errors {

View File

@@ -1,5 +1,6 @@
mod bitcoin;
mod descriptor;
mod electrum;
mod error;
mod esplora;
mod keys;
@@ -7,13 +8,16 @@ mod types;
mod wallet;
use crate::bitcoin::Address;
use crate::bitcoin::Amount;
use crate::bitcoin::FeeRate;
use crate::bitcoin::OutPoint;
use crate::bitcoin::Psbt;
use crate::bitcoin::Script;
use crate::bitcoin::Transaction;
use crate::bitcoin::TxIn;
use crate::bitcoin::TxOut;
use crate::descriptor::Descriptor;
use crate::electrum::ElectrumClient;
use crate::error::AddressError;
use crate::error::Bip32Error;
use crate::error::Bip39Error;
@@ -22,9 +26,11 @@ use crate::error::CannotConnectError;
use crate::error::CreateTxError;
use crate::error::DescriptorError;
use crate::error::DescriptorKeyError;
use crate::error::ElectrumError;
use crate::error::EsploraError;
use crate::error::ExtractTxError;
use crate::error::FeeRateError;
use crate::error::ParseAmountError;
use crate::error::PersistenceError;
use crate::error::PsbtParseError;
use crate::error::SignerError;

View File

@@ -11,6 +11,8 @@ use bdk::LocalOutput as BdkLocalOutput;
use std::sync::{Arc, Mutex};
use crate::bitcoin::Amount;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainPosition {
Confirmed { height: u32, timestamp: u64 },
@@ -45,7 +47,7 @@ impl From<BdkCanonicalTx<'_, Arc<bdk::bitcoin::Transaction>, ConfirmationTimeHei
pub struct ScriptAmount {
pub script: Arc<Script>,
pub amount: u64,
pub amount: Arc<Amount>,
}
pub struct AddressInfo {
@@ -65,23 +67,23 @@ impl From<BdkAddressInfo> for AddressInfo {
}
pub struct Balance {
pub immature: u64,
pub trusted_pending: u64,
pub untrusted_pending: u64,
pub confirmed: u64,
pub trusted_spendable: u64,
pub total: u64,
pub immature: Arc<Amount>,
pub trusted_pending: Arc<Amount>,
pub untrusted_pending: Arc<Amount>,
pub confirmed: Arc<Amount>,
pub trusted_spendable: Arc<Amount>,
pub total: Arc<Amount>,
}
impl From<BdkBalance> for Balance {
fn from(bdk_balance: BdkBalance) -> Self {
Balance {
immature: bdk_balance.immature,
trusted_pending: bdk_balance.trusted_pending,
untrusted_pending: bdk_balance.untrusted_pending,
confirmed: bdk_balance.confirmed,
trusted_spendable: bdk_balance.trusted_spendable(),
total: bdk_balance.total(),
immature: Arc::new(bdk_balance.immature.into()),
trusted_pending: Arc::new(bdk_balance.trusted_pending.into()),
untrusted_pending: Arc::new(bdk_balance.untrusted_pending.into()),
confirmed: Arc::new(bdk_balance.confirmed.into()),
trusted_spendable: Arc::new(bdk_balance.trusted_spendable().into()),
total: Arc::new(bdk_balance.total().into()),
}
}
}

View File

@@ -1,13 +1,15 @@
use crate::bitcoin::Amount;
use crate::bitcoin::{FeeRate, OutPoint, Psbt, Script, Transaction};
use crate::descriptor::Descriptor;
use crate::error::{
CalculateFeeError, CannotConnectError, CreateTxError, PersistenceError, SignerError,
TxidParseError, WalletCreationError,
CalculateFeeError, CannotConnectError, CreateTxError, DescriptorError, PersistenceError,
SignerError, TxidParseError, WalletCreationError,
};
use crate::types::{
AddressInfo, Balance, CanonicalTx, FullScanRequest, LocalOutput, ScriptAmount, SyncRequest,
};
use bdk::bitcoin::amount::Amount as BdkAmount;
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::Network;
use bdk::bitcoin::Psbt as BdkPsbt;
@@ -47,6 +49,22 @@ impl Wallet {
})
}
pub fn new_no_persist(
descriptor: Arc<Descriptor>,
change_descriptor: Option<Arc<Descriptor>>,
network: Network,
) -> Result<Self, DescriptorError> {
let descriptor = descriptor.as_string_private();
let change_descriptor = change_descriptor.map(|d| d.as_string_private());
let wallet: BdkWallet =
BdkWallet::new_no_persist(&descriptor, change_descriptor.as_ref(), network)?;
Ok(Wallet {
inner_mutex: Mutex::new(wallet),
})
}
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet")
}
@@ -82,7 +100,7 @@ impl Wallet {
}
pub fn get_balance(&self) -> Balance {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance();
let bdk_balance = self.get_wallet().get_balance();
Balance::from(bdk_balance)
}
@@ -102,8 +120,11 @@ impl Wallet {
}
pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues {
let (sent, received): (u64, u64) = self.get_wallet().sent_and_received(&tx.into());
SentAndReceivedValues { sent, received }
let (sent, received) = self.get_wallet().sent_and_received(&tx.into());
SentAndReceivedValues {
sent: Arc::new(sent.into()),
received: Arc::new(received.into()),
}
}
pub fn transactions(&self) -> Vec<CanonicalTx> {
@@ -152,15 +173,15 @@ impl Wallet {
}
pub struct SentAndReceivedValues {
pub sent: u64,
pub received: u64,
pub sent: Arc<Amount>,
pub received: Arc<Amount>,
}
pub struct Update(pub(crate) BdkUpdate);
#[derive(Clone, Debug)]
pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>,
pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>,
pub(crate) utxos: Vec<OutPoint>,
pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) change_policy: ChangeSpendPolicy,
@@ -190,9 +211,9 @@ impl TxBuilder {
}
}
pub(crate) fn add_recipient(&self, script: &Script, amount: u64) -> Arc<Self> {
let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone();
recipients.append(&mut vec![(script.0.clone(), amount)]);
pub(crate) fn add_recipient(&self, script: &Script, amount: Arc<Amount>) -> Arc<Self> {
let mut recipients: Vec<(BdkScriptBuf, BdkAmount)> = self.recipients.clone();
recipients.append(&mut vec![(script.0.clone(), amount.0)]);
Arc::new(TxBuilder {
recipients,
@@ -203,7 +224,7 @@ impl TxBuilder {
pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> {
let recipients = recipients
.iter()
.map(|script_amount| (script_amount.script.0.clone(), script_amount.amount))
.map(|script_amount| (script_amount.script.0.clone(), script_amount.amount.0)) //;
.collect();
Arc::new(TxBuilder {
recipients,

View File

@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=true
kotlin.code.style=official
libraryVersion=1.0.0-alpha.10-SNAPSHOT
libraryVersion=1.0.0-alpha.11

View File

@@ -0,0 +1,35 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602"
class LiveElectrumClientTest {
@Test
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.SIGNET)
val electrumClient: ElectrumClient = ElectrumClient(SIGNET_ELECTRUM_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = electrumClient.fullScan(fullScanRequest, 10uL, 10uL, false)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.transaction.txid()}")
println("Sent ${sentAndReceived.sent.toSat()}")
println("Received ${sentAndReceived.received.toSat()}")
}
}
}

View File

@@ -0,0 +1,36 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveMemoryWalletTest {
@Test
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx.transaction)
println("Transaction: ${tx.transaction.txid()}")
println("Sent ${sentAndReceived.sent.toSat()}")
println("Received ${sentAndReceived.received.toSat()}")
}
}
}

View File

@@ -0,0 +1,39 @@
package org.bitcoindevkit
import kotlin.test.Test
private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveTransactionTests {
@Test
fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Wallet balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val transaction: Transaction = wallet.transactions().first().transaction
println("First transaction:")
println("Txid: ${transaction.txid()}")
println("Version: ${transaction.version()}")
println("Total size: ${transaction.totalSize()}")
println("Vsize: ${transaction.vsize()}")
println("Weight: ${transaction.weight()}")
println("Coinbase transaction: ${transaction.isCoinbase()}")
println("Is explicitly RBF: ${transaction.isExplicitlyRbf()}")
println("Inputs: ${transaction.input()}")
println("Outputs: ${transaction.output()}")
}
}

View File

@@ -31,13 +31,15 @@ class LiveTxBuilderTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet)
@@ -56,15 +58,17 @@ class LiveTxBuilderTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
)
val psbt: Psbt = TxBuilder()

View File

@@ -31,9 +31,11 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL)
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3)
@@ -54,17 +56,16 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()}")
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total > 0uL) {
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL)
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet)

View File

@@ -70,7 +70,7 @@ class OfflineWalletTest {
assertEquals(
expected = 0uL,
actual = wallet.getBalance().total
actual = wallet.getBalance().total.toSat()
)
}
}

View File

@@ -18,7 +18,7 @@ import bdkpython as bdk
setup(
name="bdkpython",
version="1.0.0a10.dev",
version="1.0.0a11",
description="The Python language bindings for the Bitcoin Development Kit",
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",

View File

@@ -13,7 +13,7 @@ class LiveTxBuilderTest(unittest.TestCase):
def test_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET
)
wallet: bdk.Wallet = bdk.Wallet(
@@ -32,29 +32,29 @@ class LiveTxBuilderTest(unittest.TestCase):
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(
wallet.get_balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
)
recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
)
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
def complex_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.SIGNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET
)
wallet: bdk.Wallet = bdk.Wallet(
descriptor,
change_descriptor,
None,
"./bdk_persistence.db",
bdk.Network.SIGNET
)
@@ -68,7 +68,11 @@ class LiveTxBuilderTest(unittest.TestCase):
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(
wallet.get_balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
)
recipient1 = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",

View File

@@ -32,15 +32,19 @@ class LiveWalletTest(unittest.TestCase):
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(
wallet.get_balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
)
print(f"Transactions count: {len(wallet.transactions())}")
transactions = wallet.transactions()[:3]
for tx in transactions:
sent_and_received = wallet.sent_and_received(tx.transaction)
print(f"Transaction: {tx.transaction.txid()}")
print(f"Sent {sent_and_received.sent}")
print(f"Received {sent_and_received.received}")
print(f"Sent {sent_and_received.sent.to_sat()}")
print(f"Received {sent_and_received.received.to_sat()}")
def test_broadcast_transaction(self):
@@ -64,15 +68,18 @@ class LiveWalletTest(unittest.TestCase):
wallet.apply_update(update)
wallet.commit()
self.assertGreater(wallet.get_balance().total, 0)
self.assertGreater(
wallet.get_balance().total.to_sat(),
0,
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
)
recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET
)
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
# print(psbt.serialize())
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
walletDidSign = wallet.sign(psbt)

View File

@@ -40,7 +40,7 @@ class OfflineWalletTest(unittest.TestCase):
bdk.Network.TESTNET
)
self.assertEqual(wallet.get_balance().total, 0)
self.assertEqual(wallet.get_balance().total.to_sat(), 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,44 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ELECTRUM_URL = "ssl://mempool.space:60602"
final class LiveElectrumClientTests: XCTestCase {
func testSyncedBalance() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
network: .signet
)
let electrumClient: ElectrumClient = try ElectrumClient(url: SIGNET_ELECTRUM_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try electrumClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
batchSize: 10,
fetchPrevTxouts: false
)
try wallet.applyUpdate(update: update)
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3)
for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.txid())")
print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received.toSat())")
}
}
}

View File

@@ -0,0 +1,44 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveMemoryWalletTests: XCTestCase {
func testSyncedBalance() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3)
for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.txid())")
print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received.toSat())")
}
}
}

View File

@@ -0,0 +1,51 @@
import XCTest
@testable import BitcoinDevKit
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTransactionTests: XCTestCase {
func testSyncedBalance() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: nil,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let fullScanRequest: FullScanRequest = wallet.startFullScan()
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 10,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
guard let transaction = wallet.transactions().first?.transaction else {
print("No transactions found")
return
}
print("First transaction:")
print("Txid: \(transaction.txid())")
print("Version: \(transaction.version())")
print("Total size: \(transaction.totalSize())")
print("Vsize: \(transaction.vsize())")
print("Weight: \(transaction.weight())")
print("Coinbase transaction: \(transaction.isCoinbase())")
print("Is explicitly RBF: \(transaction.isExplicitlyRbf())")
print("Inputs: \(transaction.input())")
print("Outputs: \(transaction.output())")
}
}

View File

@@ -1,8 +1,8 @@
import XCTest
@testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTxBuilderTests: XCTestCase {
var dbFilePath: URL!
@@ -45,13 +45,18 @@ final class LiveTxBuilderTests: XCTestCase {
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
try wallet.commit()
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet)
@@ -82,15 +87,20 @@ final class LiveTxBuilderTests: XCTestCase {
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet)
let allRecipients: [ScriptAmount] = [
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200),
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200)
ScriptAmount(script: recipient1.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200)),
ScriptAmount(script: recipient2.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
]
let psbt: Psbt = try TxBuilder()
@@ -100,7 +110,7 @@ final class LiveTxBuilderTests: XCTestCase {
.enableRbf()
.finish(wallet: wallet)
try! wallet.sign(psbt: psbt)
let _ = try! wallet.sign(psbt: psbt)
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
}

View File

@@ -1,8 +1,8 @@
import XCTest
@testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveWalletTests: XCTestCase {
var dbFilePath: URL!
@@ -45,17 +45,22 @@ final class LiveWalletTests: XCTestCase {
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
try wallet.commit()
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0))
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3)
for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.txid())")
print("Sent \(sentAndReceived.sent)")
print("Received \(sentAndReceived.received)")
print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received.toSat())")
}
}
@@ -78,16 +83,21 @@ final class LiveWalletTests: XCTestCase {
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
try wallet.commit()
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
print("Balance: \(wallet.getBalance().total)")
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try
TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet)

View File

@@ -60,6 +60,6 @@ final class OfflineWalletTests: XCTestCase {
network: .testnet
)
XCTAssertEqual(wallet.getBalance().total, 0)
XCTAssertEqual(wallet.getBalance().total.toSat(), 0)
}
}