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). 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] ## [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. 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.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
kotlin.code.style=official 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) @RunWith(AndroidJUnit4::class)
class LiveTxBuilderTest { class LiveTxBuilderTest {
private val persistenceFilePath = InstrumentationRegistry private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db" .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence3.db"
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@@ -28,18 +28,20 @@ class LiveTxBuilderTest {
fun testTxBuilder() { fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val wallet = Wallet(descriptor, null, persistenceFilePath, 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 fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() 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 recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)
@@ -51,21 +53,23 @@ class LiveTxBuilderTest {
fun complexTxBuilder() { fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET) val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET) val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.SIGNET)
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val fullScanRequest: FullScanRequest = wallet.startFullScan() val fullScanRequest: FullScanRequest = wallet.startFullScan()
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() 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 recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET) val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf( val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL), ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), 4200uL), ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
) )
val psbt: Psbt = TxBuilder() val psbt: Psbt = TxBuilder()

View File

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

View File

@@ -13,7 +13,7 @@ import kotlin.test.AfterTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OfflineWalletTest { class OfflineWalletTest {
private val persistenceFilePath = InstrumentationRegistry private val persistenceFilePath = InstrumentationRegistry
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db" .getInstrumentation().targetContext.filesDir.path + "/bdk_persistence1.db"
@AfterTest @AfterTest
fun cleanup() { fun cleanup() {
@@ -72,7 +72,7 @@ class OfflineWalletTest {
assertEquals( assertEquals(
expected = 0uL, 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]] [[package]]
name = "bdk" name = "bdk"
version = "1.0.0-alpha.10" version = "1.0.0-alpha.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fc0ebc2a63463f709cfdfbb7e7877b9975bcaea9d2d4f02f97ad012de37e3b" checksum = "65c23f2903ac5dbb7b35934ae319aadc946201e4fa51b652440bd1c8fa3080ee"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bdk_chain", "bdk_chain",
@@ -157,10 +157,11 @@ dependencies = [
[[package]] [[package]]
name = "bdk-ffi" name = "bdk-ffi"
version = "1.0.0-alpha.10" version = "1.0.0-alpha.11"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"bdk", "bdk",
"bdk_electrum",
"bdk_esplora", "bdk_esplora",
"bdk_file_store", "bdk_file_store",
"bitcoin-internals", "bitcoin-internals",
@@ -170,9 +171,9 @@ dependencies = [
[[package]] [[package]]
name = "bdk_chain" name = "bdk_chain"
version = "0.13.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e879c03ebf3a64643295152a19a8b0e0a3af22e25539d2bc56ce07d07b059c33" checksum = "440ec5b1c8911f126b540e05c98493b699b497a3cb90c5e9c5eee21cdd8d1e01"
dependencies = [ dependencies = [
"bitcoin", "bitcoin",
"miniscript", "miniscript",
@@ -180,10 +181,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bdk_esplora" name = "bdk_electrum"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"bdk_chain", "bdk_chain",
"esplora-client", "esplora-client",
@@ -191,9 +202,9 @@ dependencies = [
[[package]] [[package]]
name = "bdk_file_store" name = "bdk_file_store"
version = "0.10.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "492a011ee853773bce14f2d899fa34fe3ac3b5f39eeb1504d0d2b28de448bd73" checksum = "5dfd7e9a5edb8d384ea1836b0bcd4febdd3211815acc058d64c7e284776d69ab"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bdk_chain", "bdk_chain",
@@ -204,9 +215,9 @@ dependencies = [
[[package]] [[package]]
name = "bdk_persist" name = "bdk_persist"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f7d6b38071ee828329434f86799e0bb6aaa5a4256e225480c2c53b7b2df295" checksum = "aba103c2108dd0f0b452650043d21c449ae07ce866dbaea29a9c59899a5964f0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bdk_chain", "bdk_chain",
@@ -286,6 +297,12 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.6.0"
@@ -382,6 +399,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 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]] [[package]]
name = "esplora-client" name = "esplora-client"
version = "0.7.0" version = "0.7.0"
@@ -1129,6 +1163,28 @@ dependencies = [
"nom", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

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

View File

@@ -101,6 +101,27 @@ interface DescriptorKeyError {
Bip32(string error_message); 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] [Error]
interface EsploraError { interface EsploraError {
Minreq(string error_message); Minreq(string error_message);
@@ -131,6 +152,19 @@ enum FeeRateError {
"ArithmeticOverflow" "ArithmeticOverflow"
}; };
[Error]
interface ParseAmountError {
Negative();
TooBig();
TooPrecise();
InvalidFormat();
InputTooLarge();
InvalidCharacter(string error_message);
UnknownDenomination(string error_message);
PossiblyConfusingDenomination(string error_message);
OtherParseAmountErr();
};
[Error] [Error]
interface PersistenceError { interface PersistenceError {
Write(string error_message); Write(string error_message);
@@ -185,6 +219,7 @@ interface WalletCreationError {
NotInitialized(); NotInitialized();
LoadedGenesisDoesNotMatch(string expected, string got); LoadedGenesisDoesNotMatch(string expected, string got);
LoadedNetworkDoesNotMatch(Network expected, Network? got); LoadedNetworkDoesNotMatch(Network expected, Network? got);
LoadedDescriptorDoesNotMatch(string got, KeychainKind keychain);
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -203,17 +238,17 @@ dictionary AddressInfo {
}; };
dictionary Balance { 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 { dictionary LocalOutput {
@@ -257,6 +292,9 @@ interface Wallet {
[Throws=WalletCreationError] [Throws=WalletCreationError]
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network); 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] [Throws=PersistenceError]
AddressInfo reveal_next_address(KeychainKind keychain); AddressInfo reveal_next_address(KeychainKind keychain);
@@ -302,7 +340,7 @@ interface Update {};
interface TxBuilder { interface TxBuilder {
constructor(); constructor();
TxBuilder add_recipient([ByRef] Script script, u64 amount); TxBuilder add_recipient([ByRef] Script script, Amount amount);
TxBuilder set_recipients(sequence<ScriptAmount> recipients); TxBuilder set_recipients(sequence<ScriptAmount> recipients);
@@ -450,18 +488,36 @@ interface EsploraClient {
void broadcast([ByRef] Transaction transaction); 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 // bdk-ffi-defined types
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
dictionary ScriptAmount { dictionary ScriptAmount {
Script script; Script script;
u64 amount; Amount amount;
}; };
dictionary SentAndReceivedValues { dictionary SentAndReceivedValues {
u64 sent; Amount sent;
u64 received; Amount received;
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -526,6 +582,12 @@ interface Transaction {
sequence<u8> serialize(); sequence<u8> serialize();
u64 weight(); u64 weight();
sequence<TxIn> input();
sequence<TxOut> output();
u32 lock_time();
}; };
interface Psbt { interface Psbt {
@@ -543,6 +605,18 @@ dictionary OutPoint {
u32 vout; 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 { interface FeeRate {
[Name=from_sat_per_vb, Throws=FeeRateError] [Name=from_sat_per_vb, Throws=FeeRateError]
constructor(u64 sat_per_vb); constructor(u64 sat_per_vb);
@@ -556,3 +630,10 @@ interface FeeRate {
u64 to_sat_per_kwu(); 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 crate::error::{AddressError, FeeRateError, PsbtParseError, TransactionError};
use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked};
use bdk::bitcoin::amount::ParseAmountError;
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf; use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut; use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut;
use bdk::bitcoin::consensus::encode::serialize; use bdk::bitcoin::consensus::encode::serialize;
use bdk::bitcoin::consensus::Decodable; use bdk::bitcoin::consensus::Decodable;
use bdk::bitcoin::psbt::ExtractTxError; use bdk::bitcoin::psbt::ExtractTxError;
use bdk::bitcoin::Address as BdkAddress; use bdk::bitcoin::Address as BdkAddress;
use bdk::bitcoin::Amount as BdkAmount;
use bdk::bitcoin::FeeRate as BdkFeeRate; use bdk::bitcoin::FeeRate as BdkFeeRate;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
use bdk::bitcoin::OutPoint as BdkOutPoint; use bdk::bitcoin::OutPoint as BdkOutPoint;
use bdk::bitcoin::Psbt as BdkPsbt; use bdk::bitcoin::Psbt as BdkPsbt;
use bdk::bitcoin::Transaction as BdkTransaction; use bdk::bitcoin::Transaction as BdkTransaction;
use bdk::bitcoin::TxIn as BdkTxIn;
use bdk::bitcoin::Txid; use bdk::bitcoin::Txid;
use std::io::Cursor; use std::io::Cursor;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Script(pub(crate) BdkScriptBuf); pub struct Script(pub(crate) BdkScriptBuf);
@@ -132,6 +169,18 @@ impl Transaction {
pub fn serialize(&self) -> Vec<u8> { pub fn serialize(&self) -> Vec<u8> {
serialize(&self.0) 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 { 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)] #[derive(Debug, Clone)]
pub struct TxOut { pub struct TxOut {
pub value: u64, 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::signer::SignerError as BdkSignerError;
use bdk::wallet::tx_builder::AddUtxoError; use bdk::wallet::tx_builder::AddUtxoError;
use bdk::wallet::NewOrLoadError; use bdk::wallet::NewOrLoadError;
use bdk_electrum::electrum_client::Error as BdkElectrumError;
use bdk_esplora::esplora_client::{Error as BdkEsploraError, Error}; use bdk_esplora::esplora_client::{Error as BdkEsploraError, Error};
use bdk_file_store::FileError as BdkFileError; use bdk_file_store::FileError as BdkFileError;
use bitcoin_internals::hex::display::DisplayHex; use bitcoin_internals::hex::display::DisplayHex;
use bdk::bitcoin::amount::ParseAmountError as BdkParseAmountError;
use std::convert::TryInto; use std::convert::TryInto;
use bdk::bitcoin::address::Error as BdkAddressError; use bdk::bitcoin::address::Error as BdkAddressError;
use bdk::bitcoin::consensus::encode::Error as BdkEncodeError; use bdk::bitcoin::consensus::encode::Error as BdkEncodeError;
use bdk::bitcoin::psbt::ExtractTxError as BdkExtractTxError; use bdk::bitcoin::psbt::ExtractTxError as BdkExtractTxError;
use bdk::chain::local_chain::CannotConnectError as BdkCannotConnectError; use bdk::chain::local_chain::CannotConnectError as BdkCannotConnectError;
use bdk::KeychainKind;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// error definitions // error definitions
@@ -65,37 +69,37 @@ pub enum AddressError {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Bip32Error { pub enum Bip32Error {
#[error("Cannot derive from a hardened key")] #[error("cannot derive from a hardened key")]
CannotDeriveFromHardenedKey, CannotDeriveFromHardenedKey,
#[error("Secp256k1 error: {error_message}")] #[error("secp256k1 error: {error_message}")]
Secp256k1 { error_message: String }, Secp256k1 { error_message: String },
#[error("Invalid child number: {child_number}")] #[error("invalid child number: {child_number}")]
InvalidChildNumber { child_number: u32 }, InvalidChildNumber { child_number: u32 },
#[error("Invalid format for child number")] #[error("invalid format for child number")]
InvalidChildNumberFormat, InvalidChildNumberFormat,
#[error("Invalid derivation path format")] #[error("invalid derivation path format")]
InvalidDerivationPathFormat, InvalidDerivationPathFormat,
#[error("Unknown version: {version}")] #[error("unknown version: {version}")]
UnknownVersion { version: String }, UnknownVersion { version: String },
#[error("Wrong extended key length: {length}")] #[error("wrong extended key length: {length}")]
WrongExtendedKeyLength { length: u32 }, WrongExtendedKeyLength { length: u32 },
#[error("Base58 error: {error_message}")] #[error("base58 error: {error_message}")]
Base58 { error_message: String }, Base58 { error_message: String },
#[error("Hexadecimal conversion error: {error_message}")] #[error("hexadecimal conversion error: {error_message}")]
Hex { error_message: String }, Hex { error_message: String },
#[error("Invalid public key hex length: {length}")] #[error("invalid public key hex length: {length}")]
InvalidPublicKeyHexLength { length: u32 }, InvalidPublicKeyHexLength { length: u32 },
#[error("Unknown error: {error_message}")] #[error("unknown error: {error_message}")]
UnknownError { error_message: String }, UnknownError { error_message: String },
} }
@@ -134,70 +138,70 @@ pub enum CannotConnectError {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum CreateTxError { pub enum CreateTxError {
#[error("Descriptor error: {error_message}")] #[error("descriptor error: {error_message}")]
Descriptor { error_message: String }, Descriptor { error_message: String },
#[error("Persistence failure: {error_message}")] #[error("persistence failure: {error_message}")]
Persist { error_message: String }, Persist { error_message: String },
#[error("Policy error: {error_message}")] #[error("policy error: {error_message}")]
Policy { error_message: String }, Policy { error_message: String },
#[error("Spending policy required for {kind}")] #[error("spending policy required for {kind}")]
SpendingPolicyRequired { kind: String }, SpendingPolicyRequired { kind: String },
#[error("Unsupported version 0")] #[error("unsupported version 0")]
Version0, Version0,
#[error("Unsupported version 1 with CSV")] #[error("unsupported version 1 with csv")]
Version1Csv, Version1Csv,
#[error("Lock time conflict: requested {requested}, but required {required}")] #[error("lock time conflict: requested {requested}, but required {required}")]
LockTime { requested: String, required: String }, LockTime { requested: String, required: String },
#[error("Transaction requires RBF sequence number")] #[error("transaction requires rbf sequence number")]
RbfSequence, RbfSequence,
#[error("RBF sequence: {rbf}, CSV sequence: {csv}")] #[error("rbf sequence: {rbf}, csv sequence: {csv}")]
RbfSequenceCsv { rbf: String, csv: String }, RbfSequenceCsv { rbf: String, csv: String },
#[error("Fee too low: {required} sat required")] #[error("fee too low: {required} sat required")]
FeeTooLow { required: u64 }, FeeTooLow { required: u64 },
#[error("Fee rate too low: {required}")] #[error("fee rate too low: {required}")]
FeeRateTooLow { required: String }, FeeRateTooLow { required: String },
#[error("No UTXOs selected for the transaction")] #[error("no utxos selected for the transaction")]
NoUtxosSelected, NoUtxosSelected,
#[error("Output value below dust limit at index {index}")] #[error("output value below dust limit at index {index}")]
OutputBelowDustLimit { index: u64 }, OutputBelowDustLimit { index: u64 },
#[error("Change policy descriptor error")] #[error("change policy descriptor error")]
ChangePolicyDescriptor, ChangePolicyDescriptor,
#[error("Coin selection failed: {error_message}")] #[error("coin selection failed: {error_message}")]
CoinSelection { error_message: String }, 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 }, InsufficientFunds { needed: u64, available: u64 },
#[error("Transaction has no recipients")] #[error("transaction has no recipients")]
NoRecipients, NoRecipients,
#[error("PSBT creation error: {error_message}")] #[error("psbt creation error: {error_message}")]
Psbt { error_message: String }, Psbt { error_message: String },
#[error("Missing key origin for: {key}")] #[error("missing key origin for: {key}")]
MissingKeyOrigin { key: String }, MissingKeyOrigin { key: String },
#[error("Reference to an unknown UTXO: {outpoint}")] #[error("reference to an unknown utxo: {outpoint}")]
UnknownUtxo { outpoint: String }, UnknownUtxo { outpoint: String },
#[error("Missing non-witness UTXO for outpoint: {outpoint}")] #[error("missing non-witness utxo for outpoint: {outpoint}")]
MissingNonWitnessUtxo { outpoint: String }, MissingNonWitnessUtxo { outpoint: String },
#[error("Miniscript PSBT error: {error_message}")] #[error("miniscript psbt error: {error_message}")]
MiniscriptPsbt { error_message: String }, MiniscriptPsbt { error_message: String },
} }
@@ -224,19 +228,19 @@ pub enum DescriptorError {
#[error("invalid descriptor character: {char}")] #[error("invalid descriptor character: {char}")]
InvalidDescriptorCharacter { char: String }, InvalidDescriptorCharacter { char: String },
#[error("BIP32 error: {error_message}")] #[error("bip32 error: {error_message}")]
Bip32 { error_message: String }, Bip32 { error_message: String },
#[error("Base58 error: {error_message}")] #[error("base58 error: {error_message}")]
Base58 { error_message: String }, Base58 { error_message: String },
#[error("Key-related error: {error_message}")] #[error("key-related error: {error_message}")]
Pk { error_message: String }, Pk { error_message: String },
#[error("Miniscript error: {error_message}")] #[error("miniscript error: {error_message}")]
Miniscript { error_message: String }, Miniscript { error_message: String },
#[error("Hex decoding error: {error_message}")] #[error("hex decoding error: {error_message}")]
Hex { error_message: String }, Hex { error_message: String },
} }
@@ -252,6 +256,60 @@ pub enum DescriptorKeyError {
Bip32 { error_message: String }, 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)] #[derive(Debug, thiserror::Error)]
pub enum EsploraError { pub enum EsploraError {
#[error("minreq error: {error_message}")] #[error("minreq error: {error_message}")]
@@ -263,7 +321,7 @@ pub enum EsploraError {
#[error("parsing error: {error_message}")] #[error("parsing error: {error_message}")]
Parsing { error_message: String }, 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 }, StatusCode { error_message: String },
#[error("bitcoin encoding error: {error_message}")] #[error("bitcoin encoding error: {error_message}")]
@@ -284,10 +342,10 @@ pub enum EsploraError {
#[error("header hash not found")] #[error("header hash not found")]
HeaderHashNotFound, HeaderHashNotFound,
#[error("invalid HTTP header name: {name}")] #[error("invalid http header name: {name}")]
InvalidHttpHeaderName { name: String }, InvalidHttpHeaderName { name: String },
#[error("invalid HTTP header value: {value}")] #[error("invalid http header value: {value}")]
InvalidHttpHeaderValue { value: String }, InvalidHttpHeaderValue { value: String },
#[error("the request has already been consumed")] #[error("the request has already been consumed")]
@@ -317,6 +375,37 @@ pub enum FeeRateError {
ArithmeticOverflow, 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)] #[derive(Debug, thiserror::Error)]
pub enum PersistenceError { pub enum PersistenceError {
#[error("writing to persistence error: {error_message}")] #[error("writing to persistence error: {error_message}")]
@@ -325,61 +414,61 @@ pub enum PersistenceError {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum PsbtParseError { 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 }, PsbtEncoding { error_message: String },
#[error("error in PSBT base64 encoding: {error_message}")] #[error("error in psbt base64 encoding: {error_message}")]
Base64Encoding { error_message: String }, Base64Encoding { error_message: String },
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum SignerError { pub enum SignerError {
#[error("Missing key for signing")] #[error("missing key for signing")]
MissingKey, MissingKey,
#[error("Invalid key provided")] #[error("invalid key provided")]
InvalidKey, InvalidKey,
#[error("User canceled operation")] #[error("user canceled operation")]
UserCanceled, UserCanceled,
#[error("Input index out of range")] #[error("input index out of range")]
InputIndexOutOfRange, InputIndexOutOfRange,
#[error("Missing non-witness UTXO information")] #[error("missing non-witness utxo information")]
MissingNonWitnessUtxo, MissingNonWitnessUtxo,
#[error("Invalid non-witness UTXO information provided")] #[error("invalid non-witness utxo information provided")]
InvalidNonWitnessUtxo, InvalidNonWitnessUtxo,
#[error("Missing witness UTXO")] #[error("missing witness utxo")]
MissingWitnessUtxo, MissingWitnessUtxo,
#[error("Missing witness script")] #[error("missing witness script")]
MissingWitnessScript, MissingWitnessScript,
#[error("Missing HD keypath")] #[error("missing hd keypath")]
MissingHdKeypath, MissingHdKeypath,
#[error("Non-standard sighash type used")] #[error("non-standard sighash type used")]
NonStandardSighash, NonStandardSighash,
#[error("Invalid sighash type provided")] #[error("invalid sighash type provided")]
InvalidSighash, InvalidSighash,
#[error("Error with sighash computation: {error_message}")] #[error("error with sighash computation: {error_message}")]
SighashError { error_message: String }, SighashError { error_message: String },
#[error("Miniscript Psbt error: {error_message}")] #[error("miniscript psbt error: {error_message}")]
MiniscriptPsbt { error_message: String }, MiniscriptPsbt { error_message: String },
#[error("External error: {error_message}")] #[error("external error: {error_message}")]
External { error_message: String }, External { error_message: String },
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum TransactionError { pub enum TransactionError {
#[error("IO error")] #[error("io error")]
Io, Io,
#[error("allocation of oversized vector")] #[error("allocation of oversized vector")]
@@ -388,7 +477,7 @@ pub enum TransactionError {
#[error("invalid checksum: expected={expected} actual={actual}")] #[error("invalid checksum: expected={expected} actual={actual}")]
InvalidChecksum { expected: String, actual: String }, InvalidChecksum { expected: String, actual: String },
#[error("non-minimal varint")] #[error("non-minimal var int")]
NonMinimalVarInt, NonMinimalVarInt,
#[error("parse failed")] #[error("parse failed")]
@@ -434,6 +523,9 @@ pub enum WalletCreationError {
expected: Network, expected: Network,
got: Option<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 { impl From<ParseError> for AddressError {
fn from(error: ParseError) -> Self { fn from(error: ParseError) -> Self {
match error { 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 { impl From<std::io::Error> for PersistenceError {
fn from(error: std::io::Error) -> Self { fn from(error: std::io::Error) -> Self {
PersistenceError::Write { PersistenceError::Write {
@@ -902,6 +1061,12 @@ impl From<NewOrLoadError> for WalletCreationError {
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => { NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
WalletCreationError::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 { mod test {
use crate::error::{ use crate::error::{
AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError, AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError,
DescriptorKeyError, EsploraError, ExtractTxError, FeeRateError, PersistenceError, DescriptorKeyError, ElectrumError, EsploraError, ExtractTxError, FeeRateError,
PsbtParseError, TransactionError, TxidParseError, WalletCreationError, ParseAmountError, PersistenceError, PsbtParseError, TransactionError, TxidParseError,
WalletCreationError,
}; };
use crate::CalculateFeeError; use crate::CalculateFeeError;
use crate::OutPoint; use crate::OutPoint;
use crate::SignerError; use crate::SignerError;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
use bdk::KeychainKind;
#[test] #[test]
fn test_error_address() { fn test_error_address() {
@@ -972,57 +1139,57 @@ mod test {
let cases = vec![ let cases = vec![
( (
Bip32Error::CannotDeriveFromHardenedKey, Bip32Error::CannotDeriveFromHardenedKey,
"Cannot derive from a hardened key", "cannot derive from a hardened key",
), ),
( (
Bip32Error::Secp256k1 { Bip32Error::Secp256k1 {
error_message: "failure".to_string(), error_message: "failure".to_string(),
}, },
"Secp256k1 error: failure", "secp256k1 error: failure",
), ),
( (
Bip32Error::InvalidChildNumber { child_number: 123 }, Bip32Error::InvalidChildNumber { child_number: 123 },
"Invalid child number: 123", "invalid child number: 123",
), ),
( (
Bip32Error::InvalidChildNumberFormat, Bip32Error::InvalidChildNumberFormat,
"Invalid format for child number", "invalid format for child number",
), ),
( (
Bip32Error::InvalidDerivationPathFormat, Bip32Error::InvalidDerivationPathFormat,
"Invalid derivation path format", "invalid derivation path format",
), ),
( (
Bip32Error::UnknownVersion { Bip32Error::UnknownVersion {
version: "0x123".to_string(), version: "0x123".to_string(),
}, },
"Unknown version: 0x123", "unknown version: 0x123",
), ),
( (
Bip32Error::WrongExtendedKeyLength { length: 512 }, Bip32Error::WrongExtendedKeyLength { length: 512 },
"Wrong extended key length: 512", "wrong extended key length: 512",
), ),
( (
Bip32Error::Base58 { Bip32Error::Base58 {
error_message: "error".to_string(), error_message: "error".to_string(),
}, },
"Base58 error: error", "base58 error: error",
), ),
( (
Bip32Error::Hex { Bip32Error::Hex {
error_message: "error".to_string(), error_message: "error".to_string(),
}, },
"Hexadecimal conversion error: error", "hexadecimal conversion error: error",
), ),
( (
Bip32Error::InvalidPublicKeyHexLength { length: 66 }, Bip32Error::InvalidPublicKeyHexLength { length: 66 },
"Invalid public key hex length: 66", "invalid public key hex length: 66",
), ),
( (
Bip32Error::UnknownError { Bip32Error::UnknownError {
error_message: "mystery".to_string(), error_message: "mystery".to_string(),
}, },
"Unknown error: mystery", "unknown error: mystery",
), ),
]; ];
@@ -1110,111 +1277,111 @@ mod test {
CreateTxError::Descriptor { CreateTxError::Descriptor {
error_message: "Descriptor failure".to_string(), error_message: "Descriptor failure".to_string(),
}, },
"Descriptor error: Descriptor failure", "descriptor error: Descriptor failure",
), ),
( (
CreateTxError::Persist { CreateTxError::Persist {
error_message: "Persistence error".to_string(), error_message: "Persistence error".to_string(),
}, },
"Persistence failure: Persistence error", "persistence failure: Persistence error",
), ),
( (
CreateTxError::Policy { CreateTxError::Policy {
error_message: "Policy violation".to_string(), error_message: "Policy violation".to_string(),
}, },
"Policy error: Policy violation", "policy error: Policy violation",
), ),
( (
CreateTxError::SpendingPolicyRequired { CreateTxError::SpendingPolicyRequired {
kind: "multisig".to_string(), kind: "multisig".to_string(),
}, },
"Spending policy required for multisig", "spending policy required for multisig",
), ),
(CreateTxError::Version0, "Unsupported version 0"), (CreateTxError::Version0, "unsupported version 0"),
(CreateTxError::Version1Csv, "Unsupported version 1 with CSV"), (CreateTxError::Version1Csv, "unsupported version 1 with csv"),
( (
CreateTxError::LockTime { CreateTxError::LockTime {
requested: "today".to_string(), requested: "today".to_string(),
required: "tomorrow".to_string(), required: "tomorrow".to_string(),
}, },
"Lock time conflict: requested today, but required tomorrow", "lock time conflict: requested today, but required tomorrow",
), ),
( (
CreateTxError::RbfSequence, CreateTxError::RbfSequence,
"Transaction requires RBF sequence number", "transaction requires rbf sequence number",
), ),
( (
CreateTxError::RbfSequenceCsv { CreateTxError::RbfSequenceCsv {
rbf: "123".to_string(), rbf: "123".to_string(),
csv: "456".to_string(), csv: "456".to_string(),
}, },
"RBF sequence: 123, CSV sequence: 456", "rbf sequence: 123, csv sequence: 456",
), ),
( (
CreateTxError::FeeTooLow { required: 1000 }, CreateTxError::FeeTooLow { required: 1000 },
"Fee too low: 1000 sat required", "fee too low: 1000 sat required",
), ),
( (
CreateTxError::FeeRateTooLow { CreateTxError::FeeRateTooLow {
required: "5 sat/vB".to_string(), required: "5 sat/vB".to_string(),
}, },
"Fee rate too low: 5 sat/vB", "fee rate too low: 5 sat/vB",
), ),
( (
CreateTxError::NoUtxosSelected, CreateTxError::NoUtxosSelected,
"No UTXOs selected for the transaction", "no utxos selected for the transaction",
), ),
( (
CreateTxError::OutputBelowDustLimit { index: 2 }, CreateTxError::OutputBelowDustLimit { index: 2 },
"Output value below dust limit at index 2", "output value below dust limit at index 2",
), ),
( (
CreateTxError::ChangePolicyDescriptor, CreateTxError::ChangePolicyDescriptor,
"Change policy descriptor error", "change policy descriptor error",
), ),
( (
CreateTxError::CoinSelection { CreateTxError::CoinSelection {
error_message: "No suitable outputs".to_string(), error_message: "No suitable outputs".to_string(),
}, },
"Coin selection failed: No suitable outputs", "coin selection failed: No suitable outputs",
), ),
( (
CreateTxError::InsufficientFunds { CreateTxError::InsufficientFunds {
needed: 5000, needed: 5000,
available: 3000, 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 { CreateTxError::Psbt {
error_message: "PSBT creation failed".to_string(), error_message: "PSBT creation failed".to_string(),
}, },
"PSBT creation error: PSBT creation failed", "psbt creation error: PSBT creation failed",
), ),
( (
CreateTxError::MissingKeyOrigin { CreateTxError::MissingKeyOrigin {
key: "xpub...".to_string(), key: "xpub...".to_string(),
}, },
"Missing key origin for: xpub...", "missing key origin for: xpub...",
), ),
( (
CreateTxError::UnknownUtxo { CreateTxError::UnknownUtxo {
outpoint: "outpoint123".to_string(), outpoint: "outpoint123".to_string(),
}, },
"Reference to an unknown UTXO: outpoint123", "reference to an unknown utxo: outpoint123",
), ),
( (
CreateTxError::MissingNonWitnessUtxo { CreateTxError::MissingNonWitnessUtxo {
outpoint: "outpoint456".to_string(), outpoint: "outpoint456".to_string(),
}, },
"Missing non-witness UTXO for outpoint: outpoint456", "missing non-witness utxo for outpoint: outpoint456",
), ),
( (
CreateTxError::MiniscriptPsbt { CreateTxError::MiniscriptPsbt {
error_message: "Miniscript error".to_string(), error_message: "Miniscript error".to_string(),
}, },
"Miniscript PSBT error: Miniscript error", "miniscript psbt error: Miniscript error",
), ),
]; ];
@@ -1261,31 +1428,31 @@ mod test {
DescriptorError::Bip32 { DescriptorError::Bip32 {
error_message: "Bip32 error".to_string(), error_message: "Bip32 error".to_string(),
}, },
"BIP32 error: Bip32 error", "bip32 error: Bip32 error",
), ),
( (
DescriptorError::Base58 { DescriptorError::Base58 {
error_message: "Base58 decode error".to_string(), error_message: "Base58 decode error".to_string(),
}, },
"Base58 error: Base58 decode error", "base58 error: Base58 decode error",
), ),
( (
DescriptorError::Pk { DescriptorError::Pk {
error_message: "Public key error".to_string(), error_message: "Public key error".to_string(),
}, },
"Key-related error: Public key error", "key-related error: Public key error",
), ),
( (
DescriptorError::Miniscript { DescriptorError::Miniscript {
error_message: "Miniscript evaluation error".to_string(), error_message: "Miniscript evaluation error".to_string(),
}, },
"Miniscript error: Miniscript evaluation error", "miniscript error: Miniscript evaluation error",
), ),
( (
DescriptorError::Hex { DescriptorError::Hex {
error_message: "Hexadecimal decoding error".to_string(), 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] #[test]
fn test_error_esplora() { fn test_error_esplora() {
let cases = vec![ let cases = vec![
@@ -1337,7 +1590,7 @@ mod test {
EsploraError::StatusCode { EsploraError::StatusCode {
error_message: "code 1234567".to_string(), 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 { 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] #[test]
fn test_persistence_error() { fn test_persistence_error() {
let cases = vec![ let cases = vec![
@@ -1449,13 +1739,13 @@ mod test {
PsbtParseError::PsbtEncoding { PsbtParseError::PsbtEncoding {
error_message: "invalid PSBT structure".to_string(), 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 { PsbtParseError::Base64Encoding {
error_message: "base64 decode error".to_string(), 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] #[test]
fn test_signer_errors() { fn test_signer_errors() {
let errors = vec![ let errors = vec![
(SignerError::MissingKey, "Missing key for signing"), (SignerError::MissingKey, "missing key for signing"),
(SignerError::InvalidKey, "Invalid key provided"), (SignerError::InvalidKey, "invalid key provided"),
(SignerError::UserCanceled, "User canceled operation"), (SignerError::UserCanceled, "user canceled operation"),
( (
SignerError::InputIndexOutOfRange, SignerError::InputIndexOutOfRange,
"Input index out of range", "input index out of range",
), ),
( (
SignerError::MissingNonWitnessUtxo, SignerError::MissingNonWitnessUtxo,
"Missing non-witness UTXO information", "missing non-witness utxo information",
), ),
( (
SignerError::InvalidNonWitnessUtxo, SignerError::InvalidNonWitnessUtxo,
"Invalid non-witness UTXO information provided", "invalid non-witness utxo information provided",
), ),
(SignerError::MissingWitnessUtxo, "Missing witness UTXO"), (SignerError::MissingWitnessUtxo, "missing witness utxo"),
(SignerError::MissingWitnessScript, "Missing witness script"), (SignerError::MissingWitnessScript, "missing witness script"),
(SignerError::MissingHdKeypath, "Missing HD keypath"), (SignerError::MissingHdKeypath, "missing hd keypath"),
( (
SignerError::NonStandardSighash, 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 { SignerError::SighashError {
error_message: "dummy error".into(), error_message: "dummy error".into(),
}, },
"Error with sighash computation: dummy error", "error with sighash computation: dummy error",
), ),
( (
SignerError::MiniscriptPsbt { SignerError::MiniscriptPsbt {
error_message: "psbt issue".into(), error_message: "psbt issue".into(),
}, },
"Miniscript Psbt error: psbt issue", "miniscript psbt error: psbt issue",
), ),
( (
SignerError::External { SignerError::External {
error_message: "external error".into(), error_message: "external error".into(),
}, },
"External error: external error", "external error: external error",
), ),
]; ];
@@ -1518,7 +1808,7 @@ mod test {
#[test] #[test]
fn test_error_transaction() { fn test_error_transaction() {
let cases = vec![ let cases = vec![
(TransactionError::Io, "IO error"), (TransactionError::Io, "io error"),
( (
TransactionError::OversizedVectorAllocation, TransactionError::OversizedVectorAllocation,
"allocation of oversized vector", "allocation of oversized vector",
@@ -1530,7 +1820,7 @@ mod test {
}, },
"invalid checksum: expected=deadbeef actual=beadbeef", "invalid checksum: expected=deadbeef actual=beadbeef",
), ),
(TransactionError::NonMinimalVarInt, "non-minimal varint"), (TransactionError::NonMinimalVarInt, "non-minimal var int"),
(TransactionError::ParseFailed, "parse failed"), (TransactionError::ParseFailed, "parse failed"),
( (
TransactionError::UnsupportedSegwitFlag { flag: 1 }, TransactionError::UnsupportedSegwitFlag { flag: 1 },
@@ -1605,6 +1895,13 @@ mod test {
}, },
"loaded network type is not bitcoin, got Some(Testnet)".to_string(), "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 { for (error, expected) in errors {

View File

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

View File

@@ -11,6 +11,8 @@ use bdk::LocalOutput as BdkLocalOutput;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::bitcoin::Amount;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainPosition { pub enum ChainPosition {
Confirmed { height: u32, timestamp: u64 }, Confirmed { height: u32, timestamp: u64 },
@@ -45,7 +47,7 @@ impl From<BdkCanonicalTx<'_, Arc<bdk::bitcoin::Transaction>, ConfirmationTimeHei
pub struct ScriptAmount { pub struct ScriptAmount {
pub script: Arc<Script>, pub script: Arc<Script>,
pub amount: u64, pub amount: Arc<Amount>,
} }
pub struct AddressInfo { pub struct AddressInfo {
@@ -65,23 +67,23 @@ impl From<BdkAddressInfo> for AddressInfo {
} }
pub struct Balance { pub struct Balance {
pub immature: u64, pub immature: Arc<Amount>,
pub trusted_pending: u64, pub trusted_pending: Arc<Amount>,
pub untrusted_pending: u64, pub untrusted_pending: Arc<Amount>,
pub confirmed: u64, pub confirmed: Arc<Amount>,
pub trusted_spendable: u64, pub trusted_spendable: Arc<Amount>,
pub total: u64, pub total: Arc<Amount>,
} }
impl From<BdkBalance> for Balance { impl From<BdkBalance> for Balance {
fn from(bdk_balance: BdkBalance) -> Self { fn from(bdk_balance: BdkBalance) -> Self {
Balance { Balance {
immature: bdk_balance.immature, immature: Arc::new(bdk_balance.immature.into()),
trusted_pending: bdk_balance.trusted_pending, trusted_pending: Arc::new(bdk_balance.trusted_pending.into()),
untrusted_pending: bdk_balance.untrusted_pending, untrusted_pending: Arc::new(bdk_balance.untrusted_pending.into()),
confirmed: bdk_balance.confirmed, confirmed: Arc::new(bdk_balance.confirmed.into()),
trusted_spendable: bdk_balance.trusted_spendable(), trusted_spendable: Arc::new(bdk_balance.trusted_spendable().into()),
total: bdk_balance.total(), 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::bitcoin::{FeeRate, OutPoint, Psbt, Script, Transaction};
use crate::descriptor::Descriptor; use crate::descriptor::Descriptor;
use crate::error::{ use crate::error::{
CalculateFeeError, CannotConnectError, CreateTxError, PersistenceError, SignerError, CalculateFeeError, CannotConnectError, CreateTxError, DescriptorError, PersistenceError,
TxidParseError, WalletCreationError, SignerError, TxidParseError, WalletCreationError,
}; };
use crate::types::{ use crate::types::{
AddressInfo, Balance, CanonicalTx, FullScanRequest, LocalOutput, ScriptAmount, SyncRequest, 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::blockdata::script::ScriptBuf as BdkScriptBuf;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
use bdk::bitcoin::Psbt as BdkPsbt; 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> { pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet") self.inner_mutex.lock().expect("wallet")
} }
@@ -82,7 +100,7 @@ impl Wallet {
} }
pub fn get_balance(&self) -> Balance { 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) Balance::from(bdk_balance)
} }
@@ -102,8 +120,11 @@ impl Wallet {
} }
pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues { pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues {
let (sent, received): (u64, u64) = self.get_wallet().sent_and_received(&tx.into()); let (sent, received) = self.get_wallet().sent_and_received(&tx.into());
SentAndReceivedValues { sent, received } SentAndReceivedValues {
sent: Arc::new(sent.into()),
received: Arc::new(received.into()),
}
} }
pub fn transactions(&self) -> Vec<CanonicalTx> { pub fn transactions(&self) -> Vec<CanonicalTx> {
@@ -152,15 +173,15 @@ impl Wallet {
} }
pub struct SentAndReceivedValues { pub struct SentAndReceivedValues {
pub sent: u64, pub sent: Arc<Amount>,
pub received: u64, pub received: Arc<Amount>,
} }
pub struct Update(pub(crate) BdkUpdate); pub struct Update(pub(crate) BdkUpdate);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TxBuilder { pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>, pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>,
pub(crate) utxos: Vec<OutPoint>, pub(crate) utxos: Vec<OutPoint>,
pub(crate) unspendable: HashSet<OutPoint>, pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) change_policy: ChangeSpendPolicy, pub(crate) change_policy: ChangeSpendPolicy,
@@ -190,9 +211,9 @@ impl TxBuilder {
} }
} }
pub(crate) fn add_recipient(&self, script: &Script, amount: u64) -> Arc<Self> { pub(crate) fn add_recipient(&self, script: &Script, amount: Arc<Amount>) -> Arc<Self> {
let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone(); let mut recipients: Vec<(BdkScriptBuf, BdkAmount)> = self.recipients.clone();
recipients.append(&mut vec![(script.0.clone(), amount)]); recipients.append(&mut vec![(script.0.clone(), amount.0)]);
Arc::new(TxBuilder { Arc::new(TxBuilder {
recipients, recipients,
@@ -203,7 +224,7 @@ impl TxBuilder {
pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> { pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> {
let recipients = recipients let recipients = recipients
.iter() .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(); .collect();
Arc::new(TxBuilder { Arc::new(TxBuilder {
recipients, recipients,

View File

@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=true android.enableJetifier=true
kotlin.code.style=official 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) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() 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 recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)
@@ -56,15 +58,17 @@ class LiveTxBuilderTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() 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 recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET) val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
val allRecipients: List<ScriptAmount> = listOf( val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL), ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
ScriptAmount(recipient2.scriptPubkey(), 4200uL), ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
) )
val psbt: Psbt = TxBuilder() val psbt: Psbt = TxBuilder()

View File

@@ -31,9 +31,11 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() 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()}") println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3) val transactions = wallet.transactions().take(3)
@@ -54,17 +56,16 @@ class LiveWalletTest {
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL) val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
wallet.applyUpdate(update) wallet.applyUpdate(update)
wallet.commit() wallet.commit()
println("Balance: ${wallet.getBalance().total}") println("Balance: ${wallet.getBalance().total.toSat()}")
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()}")
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." "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 recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
val psbt: Psbt = TxBuilder() val psbt: Psbt = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) .addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
.feeRate(FeeRate.fromSatPerVb(2uL)) .feeRate(FeeRate.fromSatPerVb(2uL))
.finish(wallet) .finish(wallet)

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ class LiveTxBuilderTest(unittest.TestCase):
def test_tx_builder(self): def test_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET bdk.Network.SIGNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
@@ -32,29 +32,29 @@ class LiveTxBuilderTest(unittest.TestCase):
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit() 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( recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET 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") self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
def complex_tx_builder(self): def complex_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.SIGNET
)
change_descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
bdk.Network.SIGNET bdk.Network.SIGNET
) )
wallet: bdk.Wallet = bdk.Wallet( wallet: bdk.Wallet = bdk.Wallet(
descriptor, descriptor,
change_descriptor, None,
"./bdk_persistence.db", "./bdk_persistence.db",
bdk.Network.SIGNET bdk.Network.SIGNET
) )
@@ -68,7 +68,11 @@ class LiveTxBuilderTest(unittest.TestCase):
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit() 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( recipient1 = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",

View File

@@ -32,15 +32,19 @@ class LiveWalletTest(unittest.TestCase):
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit() 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())}") print(f"Transactions count: {len(wallet.transactions())}")
transactions = wallet.transactions()[:3] transactions = wallet.transactions()[:3]
for tx in transactions: for tx in transactions:
sent_and_received = wallet.sent_and_received(tx.transaction) sent_and_received = wallet.sent_and_received(tx.transaction)
print(f"Transaction: {tx.transaction.txid()}") print(f"Transaction: {tx.transaction.txid()}")
print(f"Sent {sent_and_received.sent}") print(f"Sent {sent_and_received.sent.to_sat()}")
print(f"Received {sent_and_received.received}") print(f"Received {sent_and_received.received.to_sat()}")
def test_broadcast_transaction(self): def test_broadcast_transaction(self):
@@ -64,15 +68,18 @@ class LiveWalletTest(unittest.TestCase):
wallet.apply_update(update) wallet.apply_update(update)
wallet.commit() 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( recipient = bdk.Address(
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network=bdk.Network.SIGNET 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) 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)
# print(psbt.serialize())
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
walletDidSign = wallet.sign(psbt) walletDidSign = wallet.sign(psbt)

View File

@@ -40,7 +40,7 @@ class OfflineWalletTest(unittest.TestCase):
bdk.Network.TESTNET bdk.Network.TESTNET
) )
self.assertEqual(wallet.get_balance().total, 0) self.assertEqual(wallet.get_balance().total.to_sat(), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.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 import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveTxBuilderTests: XCTestCase { final class LiveTxBuilderTests: XCTestCase {
var dbFilePath: URL! var dbFilePath: URL!
@@ -45,13 +45,18 @@ final class LiveTxBuilderTests: XCTestCase {
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) 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 recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try TxBuilder() 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)) .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet) .finish(wallet: wallet)
@@ -82,15 +87,20 @@ final class LiveTxBuilderTests: XCTestCase {
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) 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 recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet) let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet)
let allRecipients: [ScriptAmount] = [ let allRecipients: [ScriptAmount] = [
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200), ScriptAmount(script: recipient1.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200)),
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200) ScriptAmount(script: recipient2.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
] ]
let psbt: Psbt = try TxBuilder() let psbt: Psbt = try TxBuilder()
@@ -100,7 +110,7 @@ final class LiveTxBuilderTests: XCTestCase {
.enableRbf() .enableRbf()
.finish(wallet: wallet) .finish(wallet: wallet)
try! wallet.sign(psbt: psbt) let _ = try! wallet.sign(psbt: psbt)
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
} }

View File

@@ -1,8 +1,8 @@
import XCTest import XCTest
@testable import BitcoinDevKit @testable import BitcoinDevKit
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net" private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveWalletTests: XCTestCase { final class LiveWalletTests: XCTestCase {
var dbFilePath: URL! var dbFilePath: URL!
@@ -45,17 +45,22 @@ final class LiveWalletTests: XCTestCase {
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) 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)") print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3) let transactions = wallet.transactions().prefix(3)
for tx in transactions { for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction) let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
print("Transaction: \(tx.transaction.txid())") print("Transaction: \(tx.transaction.txid())")
print("Sent \(sentAndReceived.sent)") print("Sent \(sentAndReceived.sent.toSat())")
print("Received \(sentAndReceived.received)") print("Received \(sentAndReceived.received.toSat())")
} }
} }
@@ -78,16 +83,21 @@ final class LiveWalletTests: XCTestCase {
parallelRequests: 1 parallelRequests: 1
) )
try wallet.applyUpdate(update: update) 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)"
)
print("Balance: \(wallet.getBalance().total)") print("Balance: \(wallet.getBalance().total)")
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet) let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
let psbt: Psbt = try let psbt: Psbt = try
TxBuilder() TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) .addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2)) .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
.finish(wallet: wallet) .finish(wallet: wallet)

View File

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