Compare commits
19 Commits
fix/live-t
...
chore/bump
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5cf483223 | ||
|
|
c6174199dd | ||
|
|
9c45254c3e | ||
|
|
260a0a65b3 | ||
|
|
72985f14ad | ||
|
|
5e3e24906f | ||
|
|
c702894143 | ||
|
|
ecdd7c239b | ||
|
|
ca8a3d0471 | ||
|
|
8f4c80cb98 | ||
|
|
4aec4b0434 | ||
|
|
1913c45ef9 | ||
|
|
815fe5f62d | ||
|
|
8d30c86076 | ||
|
|
c88b33473b | ||
|
|
79e7ab73ea | ||
|
|
f169b1a52f | ||
|
|
97d9bb6fbf | ||
|
|
f27bada9c9 |
@@ -3,6 +3,9 @@ Changelog information can also be found in each release's git tag (which can be
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0-alpha.11]
|
||||
This release adds the new `Amount` type, as well as more fine-grain errors.
|
||||
|
||||
## [1.0.0-alpha.7]
|
||||
This release brings back into the 1.0 API a number of APIs from the 0.31 release, and adds the new flat file persistence feature, as well as more fine-grain errors.
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin.code.style=official
|
||||
libraryVersion=1.0.0-alpha.10-SNAPSHOT
|
||||
libraryVersion=1.0.0-alpha.11
|
||||
|
||||
@@ -14,7 +14,7 @@ private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LiveTxBuilderTest {
|
||||
private val persistenceFilePath = InstrumentationRegistry
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence3.db"
|
||||
|
||||
@AfterTest
|
||||
fun cleanup() {
|
||||
@@ -28,18 +28,20 @@ class LiveTxBuilderTest {
|
||||
fun testTxBuilder() {
|
||||
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
|
||||
val wallet = Wallet(descriptor, null, persistenceFilePath, Network.SIGNET)
|
||||
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
|
||||
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
|
||||
val fullScanRequest: FullScanRequest = wallet.startFullScan()
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
val psbt: Psbt = TxBuilder()
|
||||
.addRecipient(recipient.scriptPubkey(), 4200uL)
|
||||
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
|
||||
.feeRate(FeeRate.fromSatPerVb(2uL))
|
||||
.finish(wallet)
|
||||
|
||||
@@ -51,21 +53,23 @@ class LiveTxBuilderTest {
|
||||
fun complexTxBuilder() {
|
||||
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET)
|
||||
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET)
|
||||
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.TESTNET)
|
||||
val esploraClient = EsploraClient(SIGNET_ESPLORA_URL)
|
||||
val wallet = Wallet(externalDescriptor, changeDescriptor, persistenceFilePath, Network.SIGNET)
|
||||
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
|
||||
val fullScanRequest: FullScanRequest = wallet.startFullScan()
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
|
||||
val allRecipients: List<ScriptAmount> = listOf(
|
||||
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
|
||||
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
|
||||
ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
|
||||
ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
|
||||
)
|
||||
|
||||
val psbt: Psbt = TxBuilder()
|
||||
|
||||
@@ -14,7 +14,7 @@ private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LiveWalletTest {
|
||||
private val persistenceFilePath = InstrumentationRegistry
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence2.db"
|
||||
|
||||
@AfterTest
|
||||
fun cleanup() {
|
||||
@@ -33,11 +33,13 @@ class LiveWalletTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
val balance: Balance = wallet.getBalance()
|
||||
println("Balance: $balance")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
println("Transactions count: ${wallet.transactions().count()}")
|
||||
val transactions = wallet.transactions().take(3)
|
||||
@@ -58,17 +60,16 @@ class LiveWalletTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL) {
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."
|
||||
}
|
||||
|
||||
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
|
||||
val psbt: Psbt = TxBuilder()
|
||||
.addRecipient(recipient.scriptPubkey(), 4200uL)
|
||||
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
|
||||
.feeRate(FeeRate.fromSatPerVb(4uL))
|
||||
.finish(wallet)
|
||||
|
||||
@@ -82,7 +83,7 @@ class LiveWalletTest {
|
||||
println("Txid is: ${tx.txid()}")
|
||||
|
||||
val txFee: ULong = wallet.calculateFee(tx)
|
||||
println("Tx fee is: ${txFee}")
|
||||
println("Tx fee is: $txFee")
|
||||
|
||||
val feeRate: FeeRate = wallet.calculateFeeRate(tx)
|
||||
println("Tx fee rate is: ${feeRate.toSatPerVbCeil()} sat/vB")
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlin.test.AfterTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OfflineWalletTest {
|
||||
private val persistenceFilePath = InstrumentationRegistry
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence.db"
|
||||
.getInstrumentation().targetContext.filesDir.path + "/bdk_persistence1.db"
|
||||
|
||||
@AfterTest
|
||||
fun cleanup() {
|
||||
@@ -72,7 +72,7 @@ class OfflineWalletTest {
|
||||
|
||||
assertEquals(
|
||||
expected = 0uL,
|
||||
actual = wallet.getBalance().total
|
||||
actual = wallet.getBalance().total.toSat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
80
bdk-ffi/Cargo.lock
generated
80
bdk-ffi/Cargo.lock
generated
@@ -138,9 +138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bdk"
|
||||
version = "1.0.0-alpha.10"
|
||||
version = "1.0.0-alpha.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66fc0ebc2a63463f709cfdfbb7e7877b9975bcaea9d2d4f02f97ad012de37e3b"
|
||||
checksum = "65c23f2903ac5dbb7b35934ae319aadc946201e4fa51b652440bd1c8fa3080ee"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bdk_chain",
|
||||
@@ -157,10 +157,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bdk-ffi"
|
||||
version = "1.0.0-alpha.10"
|
||||
version = "1.0.0-alpha.11"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"bdk",
|
||||
"bdk_electrum",
|
||||
"bdk_esplora",
|
||||
"bdk_file_store",
|
||||
"bitcoin-internals",
|
||||
@@ -170,9 +171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bdk_chain"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e879c03ebf3a64643295152a19a8b0e0a3af22e25539d2bc56ce07d07b059c33"
|
||||
checksum = "440ec5b1c8911f126b540e05c98493b699b497a3cb90c5e9c5eee21cdd8d1e01"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"miniscript",
|
||||
@@ -180,10 +181,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bdk_esplora"
|
||||
version = "0.12.0"
|
||||
name = "bdk_electrum"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0aad9d99b103cd9c67ce1f4702720f2813db7aeba72abc9628ae9b00462a492"
|
||||
checksum = "44bbf3b0031651a37a48bdfab0c1d96a305b587f616593d34df9b1ff63efc4ff"
|
||||
dependencies = [
|
||||
"bdk_chain",
|
||||
"electrum-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bdk_esplora"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb5b46f8c256bc083640342bd0d35ec1963971f18800c3fee1a9189eda60ecd"
|
||||
dependencies = [
|
||||
"bdk_chain",
|
||||
"esplora-client",
|
||||
@@ -191,9 +202,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bdk_file_store"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "492a011ee853773bce14f2d899fa34fe3ac3b5f39eeb1504d0d2b28de448bd73"
|
||||
checksum = "5dfd7e9a5edb8d384ea1836b0bcd4febdd3211815acc058d64c7e284776d69ab"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bdk_chain",
|
||||
@@ -204,9 +215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bdk_persist"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f7d6b38071ee828329434f86799e0bb6aaa5a4256e225480c2c53b7b2df295"
|
||||
checksum = "aba103c2108dd0f0b452650043d21c449ae07ce866dbaea29a9c59899a5964f0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bdk_chain",
|
||||
@@ -286,6 +297,12 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
@@ -382,6 +399,23 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "electrum-client"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"byteorder",
|
||||
"libc",
|
||||
"log",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"webpki-roots",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "esplora-client"
|
||||
version = "0.7.0"
|
||||
@@ -1129,6 +1163,28 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk-ffi"
|
||||
version = "1.0.0-alpha.10"
|
||||
version = "1.0.0-alpha.11"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
edition = "2018"
|
||||
@@ -18,9 +18,10 @@ path = "uniffi-bindgen.rs"
|
||||
default = ["uniffi/cli"]
|
||||
|
||||
[dependencies]
|
||||
bdk = { version = "1.0.0-alpha.10", features = ["all-keys", "keys-bip39"] }
|
||||
bdk_esplora = { version = "0.12.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
|
||||
bdk_file_store = { version = "0.10.0" }
|
||||
bdk = { version = "1.0.0-alpha.11", features = ["all-keys", "keys-bip39"] }
|
||||
bdk_esplora = { version = "0.13.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
|
||||
bdk_electrum = { version = "0.13.0" }
|
||||
bdk_file_store = { version = "0.11.0" }
|
||||
|
||||
uniffi = { version = "=0.26.1" }
|
||||
bitcoin-internals = { version = "0.2.0", features = ["alloc"] }
|
||||
|
||||
@@ -101,6 +101,27 @@ interface DescriptorKeyError {
|
||||
Bip32(string error_message);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ElectrumError {
|
||||
IOError(string error_message);
|
||||
Json(string error_message);
|
||||
Hex(string error_message);
|
||||
Protocol(string error_message);
|
||||
Bitcoin(string error_message);
|
||||
AlreadySubscribed();
|
||||
NotSubscribed();
|
||||
InvalidResponse(string error_message);
|
||||
Message(string error_message);
|
||||
InvalidDNSNameError(string domain);
|
||||
MissingDomain();
|
||||
AllAttemptsErrored();
|
||||
SharedIOError(string error_message);
|
||||
CouldntLockReader();
|
||||
Mpsc();
|
||||
CouldNotCreateConnection(string error_message);
|
||||
RequestAlreadyConsumed();
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface EsploraError {
|
||||
Minreq(string error_message);
|
||||
@@ -131,6 +152,19 @@ enum FeeRateError {
|
||||
"ArithmeticOverflow"
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ParseAmountError {
|
||||
Negative();
|
||||
TooBig();
|
||||
TooPrecise();
|
||||
InvalidFormat();
|
||||
InputTooLarge();
|
||||
InvalidCharacter(string error_message);
|
||||
UnknownDenomination(string error_message);
|
||||
PossiblyConfusingDenomination(string error_message);
|
||||
OtherParseAmountErr();
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface PersistenceError {
|
||||
Write(string error_message);
|
||||
@@ -185,6 +219,7 @@ interface WalletCreationError {
|
||||
NotInitialized();
|
||||
LoadedGenesisDoesNotMatch(string expected, string got);
|
||||
LoadedNetworkDoesNotMatch(Network expected, Network? got);
|
||||
LoadedDescriptorDoesNotMatch(string got, KeychainKind keychain);
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -203,17 +238,17 @@ dictionary AddressInfo {
|
||||
};
|
||||
|
||||
dictionary Balance {
|
||||
u64 immature;
|
||||
Amount immature;
|
||||
|
||||
u64 trusted_pending;
|
||||
Amount trusted_pending;
|
||||
|
||||
u64 untrusted_pending;
|
||||
Amount untrusted_pending;
|
||||
|
||||
u64 confirmed;
|
||||
Amount confirmed;
|
||||
|
||||
u64 trusted_spendable;
|
||||
Amount trusted_spendable;
|
||||
|
||||
u64 total;
|
||||
Amount total;
|
||||
};
|
||||
|
||||
dictionary LocalOutput {
|
||||
@@ -257,6 +292,9 @@ interface Wallet {
|
||||
[Throws=WalletCreationError]
|
||||
constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network);
|
||||
|
||||
[Name=new_no_persist, Throws=DescriptorError]
|
||||
constructor(Descriptor descriptor, Descriptor? change_descriptor, Network network);
|
||||
|
||||
[Throws=PersistenceError]
|
||||
AddressInfo reveal_next_address(KeychainKind keychain);
|
||||
|
||||
@@ -302,7 +340,7 @@ interface Update {};
|
||||
interface TxBuilder {
|
||||
constructor();
|
||||
|
||||
TxBuilder add_recipient([ByRef] Script script, u64 amount);
|
||||
TxBuilder add_recipient([ByRef] Script script, Amount amount);
|
||||
|
||||
TxBuilder set_recipients(sequence<ScriptAmount> recipients);
|
||||
|
||||
@@ -450,18 +488,36 @@ interface EsploraClient {
|
||||
void broadcast([ByRef] Transaction transaction);
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// bdk_electrum crate
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
interface ElectrumClient {
|
||||
[Throws=ElectrumError]
|
||||
constructor(string url);
|
||||
|
||||
[Throws=ElectrumError]
|
||||
Update full_scan(FullScanRequest full_scan_request, u64 stop_gap, u64 batch_size, boolean fetch_prev_txouts);
|
||||
|
||||
[Throws=ElectrumError]
|
||||
Update sync(SyncRequest sync_request, u64 batch_size, boolean fetch_prev_txouts);
|
||||
|
||||
[Throws=ElectrumError]
|
||||
string broadcast([ByRef] Transaction transaction);
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// bdk-ffi-defined types
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
dictionary ScriptAmount {
|
||||
Script script;
|
||||
u64 amount;
|
||||
Amount amount;
|
||||
};
|
||||
|
||||
dictionary SentAndReceivedValues {
|
||||
u64 sent;
|
||||
u64 received;
|
||||
Amount sent;
|
||||
Amount received;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -526,6 +582,12 @@ interface Transaction {
|
||||
sequence<u8> serialize();
|
||||
|
||||
u64 weight();
|
||||
|
||||
sequence<TxIn> input();
|
||||
|
||||
sequence<TxOut> output();
|
||||
|
||||
u32 lock_time();
|
||||
};
|
||||
|
||||
interface Psbt {
|
||||
@@ -543,6 +605,18 @@ dictionary OutPoint {
|
||||
u32 vout;
|
||||
};
|
||||
|
||||
interface Amount {
|
||||
[Name=from_sat]
|
||||
constructor(u64 from_sat);
|
||||
|
||||
[Name=from_btc, Throws=ParseAmountError]
|
||||
constructor(f64 from_btc);
|
||||
|
||||
u64 to_sat();
|
||||
|
||||
f64 to_btc();
|
||||
};
|
||||
|
||||
interface FeeRate {
|
||||
[Name=from_sat_per_vb, Throws=FeeRateError]
|
||||
constructor(u64 sat_per_vb);
|
||||
@@ -556,3 +630,10 @@ interface FeeRate {
|
||||
|
||||
u64 to_sat_per_kwu();
|
||||
};
|
||||
|
||||
dictionary TxIn {
|
||||
OutPoint previous_output;
|
||||
Script script_sig;
|
||||
u32 sequence;
|
||||
sequence<sequence<u8>> witness;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,60 @@
|
||||
use crate::error::{AddressError, FeeRateError, PsbtParseError, TransactionError};
|
||||
|
||||
use bdk::bitcoin::address::{NetworkChecked, NetworkUnchecked};
|
||||
use bdk::bitcoin::amount::ParseAmountError;
|
||||
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
|
||||
use bdk::bitcoin::blockdata::transaction::TxOut as BdkTxOut;
|
||||
use bdk::bitcoin::consensus::encode::serialize;
|
||||
use bdk::bitcoin::consensus::Decodable;
|
||||
use bdk::bitcoin::psbt::ExtractTxError;
|
||||
use bdk::bitcoin::Address as BdkAddress;
|
||||
use bdk::bitcoin::Amount as BdkAmount;
|
||||
use bdk::bitcoin::FeeRate as BdkFeeRate;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::bitcoin::OutPoint as BdkOutPoint;
|
||||
use bdk::bitcoin::Psbt as BdkPsbt;
|
||||
use bdk::bitcoin::Transaction as BdkTransaction;
|
||||
use bdk::bitcoin::TxIn as BdkTxIn;
|
||||
use bdk::bitcoin::Txid;
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Amount(pub(crate) BdkAmount);
|
||||
|
||||
impl Amount {
|
||||
pub fn from_sat(sat: u64) -> Self {
|
||||
Amount(BdkAmount::from_sat(sat))
|
||||
}
|
||||
|
||||
pub fn from_btc(btc: f64) -> Result<Self, ParseAmountError> {
|
||||
let bdk_amount = BdkAmount::from_btc(btc).map_err(ParseAmountError::from)?;
|
||||
Ok(Amount(bdk_amount))
|
||||
}
|
||||
|
||||
pub fn to_sat(&self) -> u64 {
|
||||
self.0.to_sat()
|
||||
}
|
||||
|
||||
pub fn to_btc(&self) -> f64 {
|
||||
self.0.to_btc()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for BdkAmount {
|
||||
fn from(amount: Amount) -> Self {
|
||||
amount.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BdkAmount> for Amount {
|
||||
fn from(amount: BdkAmount) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Script(pub(crate) BdkScriptBuf);
|
||||
|
||||
@@ -132,6 +169,18 @@ impl Transaction {
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
serialize(&self.0)
|
||||
}
|
||||
|
||||
pub fn input(&self) -> Vec<TxIn> {
|
||||
self.0.input.iter().map(|tx_in| tx_in.into()).collect()
|
||||
}
|
||||
|
||||
pub fn output(&self) -> Vec<TxOut> {
|
||||
self.0.output.iter().map(|tx_out| tx_out.into()).collect()
|
||||
}
|
||||
|
||||
pub fn lock_time(&self) -> u32 {
|
||||
self.0.lock_time.to_consensus_u32()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BdkTransaction> for Transaction {
|
||||
@@ -202,6 +251,28 @@ impl From<&BdkOutPoint> for OutPoint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxIn {
|
||||
pub previous_output: OutPoint,
|
||||
pub script_sig: Arc<Script>,
|
||||
pub sequence: u32,
|
||||
pub witness: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<&BdkTxIn> for TxIn {
|
||||
fn from(tx_in: &BdkTxIn) -> Self {
|
||||
TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx_in.previous_output.txid.to_string(),
|
||||
vout: tx_in.previous_output.vout,
|
||||
},
|
||||
script_sig: Arc::new(Script(tx_in.script_sig.clone())),
|
||||
sequence: tx_in.sequence.0,
|
||||
witness: tx_in.witness.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxOut {
|
||||
pub value: u64,
|
||||
|
||||
95
bdk-ffi/src/electrum.rs
Normal file
95
bdk-ffi/src/electrum.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,20 @@ use bdk::wallet::error::CreateTxError as BdkCreateTxError;
|
||||
use bdk::wallet::signer::SignerError as BdkSignerError;
|
||||
use bdk::wallet::tx_builder::AddUtxoError;
|
||||
use bdk::wallet::NewOrLoadError;
|
||||
use bdk_electrum::electrum_client::Error as BdkElectrumError;
|
||||
use bdk_esplora::esplora_client::{Error as BdkEsploraError, Error};
|
||||
use bdk_file_store::FileError as BdkFileError;
|
||||
use bitcoin_internals::hex::display::DisplayHex;
|
||||
|
||||
use bdk::bitcoin::amount::ParseAmountError as BdkParseAmountError;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bdk::bitcoin::address::Error as BdkAddressError;
|
||||
use bdk::bitcoin::consensus::encode::Error as BdkEncodeError;
|
||||
use bdk::bitcoin::psbt::ExtractTxError as BdkExtractTxError;
|
||||
use bdk::chain::local_chain::CannotConnectError as BdkCannotConnectError;
|
||||
use bdk::KeychainKind;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// error definitions
|
||||
@@ -65,37 +69,37 @@ pub enum AddressError {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Bip32Error {
|
||||
#[error("Cannot derive from a hardened key")]
|
||||
#[error("cannot derive from a hardened key")]
|
||||
CannotDeriveFromHardenedKey,
|
||||
|
||||
#[error("Secp256k1 error: {error_message}")]
|
||||
#[error("secp256k1 error: {error_message}")]
|
||||
Secp256k1 { error_message: String },
|
||||
|
||||
#[error("Invalid child number: {child_number}")]
|
||||
#[error("invalid child number: {child_number}")]
|
||||
InvalidChildNumber { child_number: u32 },
|
||||
|
||||
#[error("Invalid format for child number")]
|
||||
#[error("invalid format for child number")]
|
||||
InvalidChildNumberFormat,
|
||||
|
||||
#[error("Invalid derivation path format")]
|
||||
#[error("invalid derivation path format")]
|
||||
InvalidDerivationPathFormat,
|
||||
|
||||
#[error("Unknown version: {version}")]
|
||||
#[error("unknown version: {version}")]
|
||||
UnknownVersion { version: String },
|
||||
|
||||
#[error("Wrong extended key length: {length}")]
|
||||
#[error("wrong extended key length: {length}")]
|
||||
WrongExtendedKeyLength { length: u32 },
|
||||
|
||||
#[error("Base58 error: {error_message}")]
|
||||
#[error("base58 error: {error_message}")]
|
||||
Base58 { error_message: String },
|
||||
|
||||
#[error("Hexadecimal conversion error: {error_message}")]
|
||||
#[error("hexadecimal conversion error: {error_message}")]
|
||||
Hex { error_message: String },
|
||||
|
||||
#[error("Invalid public key hex length: {length}")]
|
||||
#[error("invalid public key hex length: {length}")]
|
||||
InvalidPublicKeyHexLength { length: u32 },
|
||||
|
||||
#[error("Unknown error: {error_message}")]
|
||||
#[error("unknown error: {error_message}")]
|
||||
UnknownError { error_message: String },
|
||||
}
|
||||
|
||||
@@ -134,70 +138,70 @@ pub enum CannotConnectError {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CreateTxError {
|
||||
#[error("Descriptor error: {error_message}")]
|
||||
#[error("descriptor error: {error_message}")]
|
||||
Descriptor { error_message: String },
|
||||
|
||||
#[error("Persistence failure: {error_message}")]
|
||||
#[error("persistence failure: {error_message}")]
|
||||
Persist { error_message: String },
|
||||
|
||||
#[error("Policy error: {error_message}")]
|
||||
#[error("policy error: {error_message}")]
|
||||
Policy { error_message: String },
|
||||
|
||||
#[error("Spending policy required for {kind}")]
|
||||
#[error("spending policy required for {kind}")]
|
||||
SpendingPolicyRequired { kind: String },
|
||||
|
||||
#[error("Unsupported version 0")]
|
||||
#[error("unsupported version 0")]
|
||||
Version0,
|
||||
|
||||
#[error("Unsupported version 1 with CSV")]
|
||||
#[error("unsupported version 1 with csv")]
|
||||
Version1Csv,
|
||||
|
||||
#[error("Lock time conflict: requested {requested}, but required {required}")]
|
||||
#[error("lock time conflict: requested {requested}, but required {required}")]
|
||||
LockTime { requested: String, required: String },
|
||||
|
||||
#[error("Transaction requires RBF sequence number")]
|
||||
#[error("transaction requires rbf sequence number")]
|
||||
RbfSequence,
|
||||
|
||||
#[error("RBF sequence: {rbf}, CSV sequence: {csv}")]
|
||||
#[error("rbf sequence: {rbf}, csv sequence: {csv}")]
|
||||
RbfSequenceCsv { rbf: String, csv: String },
|
||||
|
||||
#[error("Fee too low: {required} sat required")]
|
||||
#[error("fee too low: {required} sat required")]
|
||||
FeeTooLow { required: u64 },
|
||||
|
||||
#[error("Fee rate too low: {required}")]
|
||||
#[error("fee rate too low: {required}")]
|
||||
FeeRateTooLow { required: String },
|
||||
|
||||
#[error("No UTXOs selected for the transaction")]
|
||||
#[error("no utxos selected for the transaction")]
|
||||
NoUtxosSelected,
|
||||
|
||||
#[error("Output value below dust limit at index {index}")]
|
||||
#[error("output value below dust limit at index {index}")]
|
||||
OutputBelowDustLimit { index: u64 },
|
||||
|
||||
#[error("Change policy descriptor error")]
|
||||
#[error("change policy descriptor error")]
|
||||
ChangePolicyDescriptor,
|
||||
|
||||
#[error("Coin selection failed: {error_message}")]
|
||||
#[error("coin selection failed: {error_message}")]
|
||||
CoinSelection { error_message: String },
|
||||
|
||||
#[error("Insufficient funds: needed {needed} sat, available {available} sat")]
|
||||
#[error("insufficient funds: needed {needed} sat, available {available} sat")]
|
||||
InsufficientFunds { needed: u64, available: u64 },
|
||||
|
||||
#[error("Transaction has no recipients")]
|
||||
#[error("transaction has no recipients")]
|
||||
NoRecipients,
|
||||
|
||||
#[error("PSBT creation error: {error_message}")]
|
||||
#[error("psbt creation error: {error_message}")]
|
||||
Psbt { error_message: String },
|
||||
|
||||
#[error("Missing key origin for: {key}")]
|
||||
#[error("missing key origin for: {key}")]
|
||||
MissingKeyOrigin { key: String },
|
||||
|
||||
#[error("Reference to an unknown UTXO: {outpoint}")]
|
||||
#[error("reference to an unknown utxo: {outpoint}")]
|
||||
UnknownUtxo { outpoint: String },
|
||||
|
||||
#[error("Missing non-witness UTXO for outpoint: {outpoint}")]
|
||||
#[error("missing non-witness utxo for outpoint: {outpoint}")]
|
||||
MissingNonWitnessUtxo { outpoint: String },
|
||||
|
||||
#[error("Miniscript PSBT error: {error_message}")]
|
||||
#[error("miniscript psbt error: {error_message}")]
|
||||
MiniscriptPsbt { error_message: String },
|
||||
}
|
||||
|
||||
@@ -224,19 +228,19 @@ pub enum DescriptorError {
|
||||
#[error("invalid descriptor character: {char}")]
|
||||
InvalidDescriptorCharacter { char: String },
|
||||
|
||||
#[error("BIP32 error: {error_message}")]
|
||||
#[error("bip32 error: {error_message}")]
|
||||
Bip32 { error_message: String },
|
||||
|
||||
#[error("Base58 error: {error_message}")]
|
||||
#[error("base58 error: {error_message}")]
|
||||
Base58 { error_message: String },
|
||||
|
||||
#[error("Key-related error: {error_message}")]
|
||||
#[error("key-related error: {error_message}")]
|
||||
Pk { error_message: String },
|
||||
|
||||
#[error("Miniscript error: {error_message}")]
|
||||
#[error("miniscript error: {error_message}")]
|
||||
Miniscript { error_message: String },
|
||||
|
||||
#[error("Hex decoding error: {error_message}")]
|
||||
#[error("hex decoding error: {error_message}")]
|
||||
Hex { error_message: String },
|
||||
}
|
||||
|
||||
@@ -252,6 +256,60 @@ pub enum DescriptorKeyError {
|
||||
Bip32 { error_message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ElectrumError {
|
||||
#[error("{error_message}")]
|
||||
IOError { error_message: String },
|
||||
|
||||
#[error("{error_message}")]
|
||||
Json { error_message: String },
|
||||
|
||||
#[error("{error_message}")]
|
||||
Hex { error_message: String },
|
||||
|
||||
#[error("electrum server error: {error_message}")]
|
||||
Protocol { error_message: String },
|
||||
|
||||
#[error("{error_message}")]
|
||||
Bitcoin { error_message: String },
|
||||
|
||||
#[error("already subscribed to the notifications of an address")]
|
||||
AlreadySubscribed,
|
||||
|
||||
#[error("not subscribed to the notifications of an address")]
|
||||
NotSubscribed,
|
||||
|
||||
#[error("error during the deserialization of a response from the server: {error_message}")]
|
||||
InvalidResponse { error_message: String },
|
||||
|
||||
#[error("{error_message}")]
|
||||
Message { error_message: String },
|
||||
|
||||
#[error("invalid domain name {domain} not matching SSL certificate")]
|
||||
InvalidDNSNameError { domain: String },
|
||||
|
||||
#[error("missing domain while it was explicitly asked to validate it")]
|
||||
MissingDomain,
|
||||
|
||||
#[error("made one or multiple attempts, all errored")]
|
||||
AllAttemptsErrored,
|
||||
|
||||
#[error("{error_message}")]
|
||||
SharedIOError { error_message: String },
|
||||
|
||||
#[error("couldn't take a lock on the reader mutex. This means that there's already another reader thread is running")]
|
||||
CouldntLockReader,
|
||||
|
||||
#[error("broken IPC communication channel: the other thread probably has exited")]
|
||||
Mpsc,
|
||||
|
||||
#[error("{error_message}")]
|
||||
CouldNotCreateConnection { error_message: String },
|
||||
|
||||
#[error("the request has already been consumed")]
|
||||
RequestAlreadyConsumed,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EsploraError {
|
||||
#[error("minreq error: {error_message}")]
|
||||
@@ -263,7 +321,7 @@ pub enum EsploraError {
|
||||
#[error("parsing error: {error_message}")]
|
||||
Parsing { error_message: String },
|
||||
|
||||
#[error("Invalid status code, unable to convert to u16: {error_message}")]
|
||||
#[error("invalid status code, unable to convert to u16: {error_message}")]
|
||||
StatusCode { error_message: String },
|
||||
|
||||
#[error("bitcoin encoding error: {error_message}")]
|
||||
@@ -284,10 +342,10 @@ pub enum EsploraError {
|
||||
#[error("header hash not found")]
|
||||
HeaderHashNotFound,
|
||||
|
||||
#[error("invalid HTTP header name: {name}")]
|
||||
#[error("invalid http header name: {name}")]
|
||||
InvalidHttpHeaderName { name: String },
|
||||
|
||||
#[error("invalid HTTP header value: {value}")]
|
||||
#[error("invalid http header value: {value}")]
|
||||
InvalidHttpHeaderValue { value: String },
|
||||
|
||||
#[error("the request has already been consumed")]
|
||||
@@ -317,6 +375,37 @@ pub enum FeeRateError {
|
||||
ArithmeticOverflow,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseAmountError {
|
||||
#[error("amount is negative")]
|
||||
Negative,
|
||||
|
||||
#[error("amount is too large")]
|
||||
TooBig,
|
||||
|
||||
#[error("amount is too precise")]
|
||||
TooPrecise,
|
||||
|
||||
#[error("invalid amount format")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("input is too large")]
|
||||
InputTooLarge,
|
||||
|
||||
#[error("invalid character: {error_message}")]
|
||||
InvalidCharacter { error_message: String },
|
||||
|
||||
#[error("unknown denomination: {error_message}")]
|
||||
UnknownDenomination { error_message: String },
|
||||
|
||||
#[error("possibly confusing denomination: {error_message}")]
|
||||
PossiblyConfusingDenomination { error_message: String },
|
||||
|
||||
// Has to handle non-exhaustive
|
||||
#[error("unknown parse amount error")]
|
||||
OtherParseAmountErr,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PersistenceError {
|
||||
#[error("writing to persistence error: {error_message}")]
|
||||
@@ -325,61 +414,61 @@ pub enum PersistenceError {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PsbtParseError {
|
||||
#[error("error in internal PSBT data structure: {error_message}")]
|
||||
#[error("error in internal psbt data structure: {error_message}")]
|
||||
PsbtEncoding { error_message: String },
|
||||
|
||||
#[error("error in PSBT base64 encoding: {error_message}")]
|
||||
#[error("error in psbt base64 encoding: {error_message}")]
|
||||
Base64Encoding { error_message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignerError {
|
||||
#[error("Missing key for signing")]
|
||||
#[error("missing key for signing")]
|
||||
MissingKey,
|
||||
|
||||
#[error("Invalid key provided")]
|
||||
#[error("invalid key provided")]
|
||||
InvalidKey,
|
||||
|
||||
#[error("User canceled operation")]
|
||||
#[error("user canceled operation")]
|
||||
UserCanceled,
|
||||
|
||||
#[error("Input index out of range")]
|
||||
#[error("input index out of range")]
|
||||
InputIndexOutOfRange,
|
||||
|
||||
#[error("Missing non-witness UTXO information")]
|
||||
#[error("missing non-witness utxo information")]
|
||||
MissingNonWitnessUtxo,
|
||||
|
||||
#[error("Invalid non-witness UTXO information provided")]
|
||||
#[error("invalid non-witness utxo information provided")]
|
||||
InvalidNonWitnessUtxo,
|
||||
|
||||
#[error("Missing witness UTXO")]
|
||||
#[error("missing witness utxo")]
|
||||
MissingWitnessUtxo,
|
||||
|
||||
#[error("Missing witness script")]
|
||||
#[error("missing witness script")]
|
||||
MissingWitnessScript,
|
||||
|
||||
#[error("Missing HD keypath")]
|
||||
#[error("missing hd keypath")]
|
||||
MissingHdKeypath,
|
||||
|
||||
#[error("Non-standard sighash type used")]
|
||||
#[error("non-standard sighash type used")]
|
||||
NonStandardSighash,
|
||||
|
||||
#[error("Invalid sighash type provided")]
|
||||
#[error("invalid sighash type provided")]
|
||||
InvalidSighash,
|
||||
|
||||
#[error("Error with sighash computation: {error_message}")]
|
||||
#[error("error with sighash computation: {error_message}")]
|
||||
SighashError { error_message: String },
|
||||
|
||||
#[error("Miniscript Psbt error: {error_message}")]
|
||||
#[error("miniscript psbt error: {error_message}")]
|
||||
MiniscriptPsbt { error_message: String },
|
||||
|
||||
#[error("External error: {error_message}")]
|
||||
#[error("external error: {error_message}")]
|
||||
External { error_message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TransactionError {
|
||||
#[error("IO error")]
|
||||
#[error("io error")]
|
||||
Io,
|
||||
|
||||
#[error("allocation of oversized vector")]
|
||||
@@ -388,7 +477,7 @@ pub enum TransactionError {
|
||||
#[error("invalid checksum: expected={expected} actual={actual}")]
|
||||
InvalidChecksum { expected: String, actual: String },
|
||||
|
||||
#[error("non-minimal varint")]
|
||||
#[error("non-minimal var int")]
|
||||
NonMinimalVarInt,
|
||||
|
||||
#[error("parse failed")]
|
||||
@@ -434,6 +523,9 @@ pub enum WalletCreationError {
|
||||
expected: Network,
|
||||
got: Option<Network>,
|
||||
},
|
||||
|
||||
#[error("loaded descriptor '{got}' does not match what was provided '{keychain:?}'")]
|
||||
LoadedDescriptorDoesNotMatch { got: String, keychain: KeychainKind },
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -466,6 +558,51 @@ impl From<BdkAddressError> for AddressError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BdkElectrumError> for ElectrumError {
|
||||
fn from(error: BdkElectrumError) -> Self {
|
||||
match error {
|
||||
BdkElectrumError::IOError(e) => ElectrumError::IOError {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::JSON(e) => ElectrumError::Json {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::Hex(e) => ElectrumError::Hex {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::Protocol(e) => ElectrumError::Protocol {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::Bitcoin(e) => ElectrumError::Bitcoin {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::AlreadySubscribed(_) => ElectrumError::AlreadySubscribed,
|
||||
BdkElectrumError::NotSubscribed(_) => ElectrumError::NotSubscribed,
|
||||
BdkElectrumError::InvalidResponse(e) => ElectrumError::InvalidResponse {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::Message(e) => ElectrumError::Message {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::InvalidDNSNameError(domain) => {
|
||||
ElectrumError::InvalidDNSNameError { domain }
|
||||
}
|
||||
BdkElectrumError::MissingDomain => ElectrumError::MissingDomain,
|
||||
BdkElectrumError::AllAttemptsErrored(_) => ElectrumError::AllAttemptsErrored,
|
||||
BdkElectrumError::SharedIOError(e) => ElectrumError::SharedIOError {
|
||||
error_message: e.to_string(),
|
||||
},
|
||||
BdkElectrumError::CouldntLockReader => ElectrumError::CouldntLockReader,
|
||||
BdkElectrumError::Mpsc => ElectrumError::Mpsc,
|
||||
BdkElectrumError::CouldNotCreateConnection(error_message) => {
|
||||
ElectrumError::CouldNotCreateConnection {
|
||||
error_message: error_message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for AddressError {
|
||||
fn from(error: ParseError) -> Self {
|
||||
match error {
|
||||
@@ -800,6 +937,28 @@ impl From<BdkExtractTxError> for ExtractTxError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BdkParseAmountError> for ParseAmountError {
|
||||
fn from(error: BdkParseAmountError) -> Self {
|
||||
match error {
|
||||
BdkParseAmountError::Negative => ParseAmountError::Negative,
|
||||
BdkParseAmountError::TooBig => ParseAmountError::TooBig,
|
||||
BdkParseAmountError::InvalidFormat => ParseAmountError::InvalidFormat,
|
||||
BdkParseAmountError::TooPrecise => ParseAmountError::TooPrecise,
|
||||
BdkParseAmountError::InputTooLarge => ParseAmountError::InputTooLarge,
|
||||
BdkParseAmountError::InvalidCharacter(c) => ParseAmountError::InvalidCharacter {
|
||||
error_message: c.to_string(),
|
||||
},
|
||||
BdkParseAmountError::UnknownDenomination(s) => {
|
||||
ParseAmountError::UnknownDenomination { error_message: s }
|
||||
}
|
||||
BdkParseAmountError::PossiblyConfusingDenomination(s) => {
|
||||
ParseAmountError::PossiblyConfusingDenomination { error_message: s }
|
||||
}
|
||||
_ => ParseAmountError::OtherParseAmountErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PersistenceError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
PersistenceError::Write {
|
||||
@@ -902,6 +1061,12 @@ impl From<NewOrLoadError> for WalletCreationError {
|
||||
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
|
||||
WalletCreationError::LoadedNetworkDoesNotMatch { expected, got }
|
||||
}
|
||||
NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
|
||||
WalletCreationError::LoadedDescriptorDoesNotMatch {
|
||||
got: format!("{:?}", got),
|
||||
keychain,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -914,13 +1079,15 @@ impl From<NewOrLoadError> for WalletCreationError {
|
||||
mod test {
|
||||
use crate::error::{
|
||||
AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError,
|
||||
DescriptorKeyError, EsploraError, ExtractTxError, FeeRateError, PersistenceError,
|
||||
PsbtParseError, TransactionError, TxidParseError, WalletCreationError,
|
||||
DescriptorKeyError, ElectrumError, EsploraError, ExtractTxError, FeeRateError,
|
||||
ParseAmountError, PersistenceError, PsbtParseError, TransactionError, TxidParseError,
|
||||
WalletCreationError,
|
||||
};
|
||||
use crate::CalculateFeeError;
|
||||
use crate::OutPoint;
|
||||
use crate::SignerError;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::KeychainKind;
|
||||
|
||||
#[test]
|
||||
fn test_error_address() {
|
||||
@@ -972,57 +1139,57 @@ mod test {
|
||||
let cases = vec![
|
||||
(
|
||||
Bip32Error::CannotDeriveFromHardenedKey,
|
||||
"Cannot derive from a hardened key",
|
||||
"cannot derive from a hardened key",
|
||||
),
|
||||
(
|
||||
Bip32Error::Secp256k1 {
|
||||
error_message: "failure".to_string(),
|
||||
},
|
||||
"Secp256k1 error: failure",
|
||||
"secp256k1 error: failure",
|
||||
),
|
||||
(
|
||||
Bip32Error::InvalidChildNumber { child_number: 123 },
|
||||
"Invalid child number: 123",
|
||||
"invalid child number: 123",
|
||||
),
|
||||
(
|
||||
Bip32Error::InvalidChildNumberFormat,
|
||||
"Invalid format for child number",
|
||||
"invalid format for child number",
|
||||
),
|
||||
(
|
||||
Bip32Error::InvalidDerivationPathFormat,
|
||||
"Invalid derivation path format",
|
||||
"invalid derivation path format",
|
||||
),
|
||||
(
|
||||
Bip32Error::UnknownVersion {
|
||||
version: "0x123".to_string(),
|
||||
},
|
||||
"Unknown version: 0x123",
|
||||
"unknown version: 0x123",
|
||||
),
|
||||
(
|
||||
Bip32Error::WrongExtendedKeyLength { length: 512 },
|
||||
"Wrong extended key length: 512",
|
||||
"wrong extended key length: 512",
|
||||
),
|
||||
(
|
||||
Bip32Error::Base58 {
|
||||
error_message: "error".to_string(),
|
||||
},
|
||||
"Base58 error: error",
|
||||
"base58 error: error",
|
||||
),
|
||||
(
|
||||
Bip32Error::Hex {
|
||||
error_message: "error".to_string(),
|
||||
},
|
||||
"Hexadecimal conversion error: error",
|
||||
"hexadecimal conversion error: error",
|
||||
),
|
||||
(
|
||||
Bip32Error::InvalidPublicKeyHexLength { length: 66 },
|
||||
"Invalid public key hex length: 66",
|
||||
"invalid public key hex length: 66",
|
||||
),
|
||||
(
|
||||
Bip32Error::UnknownError {
|
||||
error_message: "mystery".to_string(),
|
||||
},
|
||||
"Unknown error: mystery",
|
||||
"unknown error: mystery",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1110,111 +1277,111 @@ mod test {
|
||||
CreateTxError::Descriptor {
|
||||
error_message: "Descriptor failure".to_string(),
|
||||
},
|
||||
"Descriptor error: Descriptor failure",
|
||||
"descriptor error: Descriptor failure",
|
||||
),
|
||||
(
|
||||
CreateTxError::Persist {
|
||||
error_message: "Persistence error".to_string(),
|
||||
},
|
||||
"Persistence failure: Persistence error",
|
||||
"persistence failure: Persistence error",
|
||||
),
|
||||
(
|
||||
CreateTxError::Policy {
|
||||
error_message: "Policy violation".to_string(),
|
||||
},
|
||||
"Policy error: Policy violation",
|
||||
"policy error: Policy violation",
|
||||
),
|
||||
(
|
||||
CreateTxError::SpendingPolicyRequired {
|
||||
kind: "multisig".to_string(),
|
||||
},
|
||||
"Spending policy required for multisig",
|
||||
"spending policy required for multisig",
|
||||
),
|
||||
(CreateTxError::Version0, "Unsupported version 0"),
|
||||
(CreateTxError::Version1Csv, "Unsupported version 1 with CSV"),
|
||||
(CreateTxError::Version0, "unsupported version 0"),
|
||||
(CreateTxError::Version1Csv, "unsupported version 1 with csv"),
|
||||
(
|
||||
CreateTxError::LockTime {
|
||||
requested: "today".to_string(),
|
||||
required: "tomorrow".to_string(),
|
||||
},
|
||||
"Lock time conflict: requested today, but required tomorrow",
|
||||
"lock time conflict: requested today, but required tomorrow",
|
||||
),
|
||||
(
|
||||
CreateTxError::RbfSequence,
|
||||
"Transaction requires RBF sequence number",
|
||||
"transaction requires rbf sequence number",
|
||||
),
|
||||
(
|
||||
CreateTxError::RbfSequenceCsv {
|
||||
rbf: "123".to_string(),
|
||||
csv: "456".to_string(),
|
||||
},
|
||||
"RBF sequence: 123, CSV sequence: 456",
|
||||
"rbf sequence: 123, csv sequence: 456",
|
||||
),
|
||||
(
|
||||
CreateTxError::FeeTooLow { required: 1000 },
|
||||
"Fee too low: 1000 sat required",
|
||||
"fee too low: 1000 sat required",
|
||||
),
|
||||
(
|
||||
CreateTxError::FeeRateTooLow {
|
||||
required: "5 sat/vB".to_string(),
|
||||
},
|
||||
"Fee rate too low: 5 sat/vB",
|
||||
"fee rate too low: 5 sat/vB",
|
||||
),
|
||||
(
|
||||
CreateTxError::NoUtxosSelected,
|
||||
"No UTXOs selected for the transaction",
|
||||
"no utxos selected for the transaction",
|
||||
),
|
||||
(
|
||||
CreateTxError::OutputBelowDustLimit { index: 2 },
|
||||
"Output value below dust limit at index 2",
|
||||
"output value below dust limit at index 2",
|
||||
),
|
||||
(
|
||||
CreateTxError::ChangePolicyDescriptor,
|
||||
"Change policy descriptor error",
|
||||
"change policy descriptor error",
|
||||
),
|
||||
(
|
||||
CreateTxError::CoinSelection {
|
||||
error_message: "No suitable outputs".to_string(),
|
||||
},
|
||||
"Coin selection failed: No suitable outputs",
|
||||
"coin selection failed: No suitable outputs",
|
||||
),
|
||||
(
|
||||
CreateTxError::InsufficientFunds {
|
||||
needed: 5000,
|
||||
available: 3000,
|
||||
},
|
||||
"Insufficient funds: needed 5000 sat, available 3000 sat",
|
||||
"insufficient funds: needed 5000 sat, available 3000 sat",
|
||||
),
|
||||
(CreateTxError::NoRecipients, "Transaction has no recipients"),
|
||||
(CreateTxError::NoRecipients, "transaction has no recipients"),
|
||||
(
|
||||
CreateTxError::Psbt {
|
||||
error_message: "PSBT creation failed".to_string(),
|
||||
},
|
||||
"PSBT creation error: PSBT creation failed",
|
||||
"psbt creation error: PSBT creation failed",
|
||||
),
|
||||
(
|
||||
CreateTxError::MissingKeyOrigin {
|
||||
key: "xpub...".to_string(),
|
||||
},
|
||||
"Missing key origin for: xpub...",
|
||||
"missing key origin for: xpub...",
|
||||
),
|
||||
(
|
||||
CreateTxError::UnknownUtxo {
|
||||
outpoint: "outpoint123".to_string(),
|
||||
},
|
||||
"Reference to an unknown UTXO: outpoint123",
|
||||
"reference to an unknown utxo: outpoint123",
|
||||
),
|
||||
(
|
||||
CreateTxError::MissingNonWitnessUtxo {
|
||||
outpoint: "outpoint456".to_string(),
|
||||
},
|
||||
"Missing non-witness UTXO for outpoint: outpoint456",
|
||||
"missing non-witness utxo for outpoint: outpoint456",
|
||||
),
|
||||
(
|
||||
CreateTxError::MiniscriptPsbt {
|
||||
error_message: "Miniscript error".to_string(),
|
||||
},
|
||||
"Miniscript PSBT error: Miniscript error",
|
||||
"miniscript psbt error: Miniscript error",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1261,31 +1428,31 @@ mod test {
|
||||
DescriptorError::Bip32 {
|
||||
error_message: "Bip32 error".to_string(),
|
||||
},
|
||||
"BIP32 error: Bip32 error",
|
||||
"bip32 error: Bip32 error",
|
||||
),
|
||||
(
|
||||
DescriptorError::Base58 {
|
||||
error_message: "Base58 decode error".to_string(),
|
||||
},
|
||||
"Base58 error: Base58 decode error",
|
||||
"base58 error: Base58 decode error",
|
||||
),
|
||||
(
|
||||
DescriptorError::Pk {
|
||||
error_message: "Public key error".to_string(),
|
||||
},
|
||||
"Key-related error: Public key error",
|
||||
"key-related error: Public key error",
|
||||
),
|
||||
(
|
||||
DescriptorError::Miniscript {
|
||||
error_message: "Miniscript evaluation error".to_string(),
|
||||
},
|
||||
"Miniscript error: Miniscript evaluation error",
|
||||
"miniscript error: Miniscript evaluation error",
|
||||
),
|
||||
(
|
||||
DescriptorError::Hex {
|
||||
error_message: "Hexadecimal decoding error".to_string(),
|
||||
},
|
||||
"Hex decoding error: Hexadecimal decoding error",
|
||||
"hex decoding error: Hexadecimal decoding error",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1317,6 +1484,92 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_electrum_client() {
|
||||
let cases = vec![
|
||||
(
|
||||
ElectrumError::IOError { error_message: "message".to_string(), },
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::Json { error_message: "message".to_string(), },
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::Hex { error_message: "message".to_string(), },
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::Protocol { error_message: "message".to_string(), },
|
||||
"electrum server error: message",
|
||||
),
|
||||
(
|
||||
ElectrumError::Bitcoin {
|
||||
error_message: "message".to_string(),
|
||||
},
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::AlreadySubscribed,
|
||||
"already subscribed to the notifications of an address",
|
||||
),
|
||||
(
|
||||
ElectrumError::NotSubscribed,
|
||||
"not subscribed to the notifications of an address",
|
||||
),
|
||||
(
|
||||
ElectrumError::InvalidResponse {
|
||||
error_message: "message".to_string(),
|
||||
},
|
||||
"error during the deserialization of a response from the server: message",
|
||||
),
|
||||
(
|
||||
ElectrumError::Message {
|
||||
error_message: "message".to_string(),
|
||||
},
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::InvalidDNSNameError {
|
||||
domain: "domain".to_string(),
|
||||
},
|
||||
"invalid domain name domain not matching SSL certificate",
|
||||
),
|
||||
(
|
||||
ElectrumError::MissingDomain,
|
||||
"missing domain while it was explicitly asked to validate it",
|
||||
),
|
||||
(
|
||||
ElectrumError::AllAttemptsErrored,
|
||||
"made one or multiple attempts, all errored",
|
||||
),
|
||||
(
|
||||
ElectrumError::SharedIOError {
|
||||
error_message: "message".to_string(),
|
||||
},
|
||||
"message",
|
||||
),
|
||||
(
|
||||
ElectrumError::CouldntLockReader,
|
||||
"couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"
|
||||
),
|
||||
(
|
||||
ElectrumError::Mpsc,
|
||||
"broken IPC communication channel: the other thread probably has exited",
|
||||
),
|
||||
(
|
||||
ElectrumError::CouldNotCreateConnection {
|
||||
error_message: "message".to_string(),
|
||||
},
|
||||
"message",
|
||||
)
|
||||
];
|
||||
|
||||
for (error, expected_message) in cases {
|
||||
assert_eq!(error.to_string(), expected_message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_esplora() {
|
||||
let cases = vec![
|
||||
@@ -1337,7 +1590,7 @@ mod test {
|
||||
EsploraError::StatusCode {
|
||||
error_message: "code 1234567".to_string(),
|
||||
},
|
||||
"Invalid status code, unable to convert to u16: code 1234567",
|
||||
"invalid status code, unable to convert to u16: code 1234567",
|
||||
),
|
||||
(
|
||||
EsploraError::Parsing {
|
||||
@@ -1418,6 +1671,43 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_parse_amount() {
|
||||
let cases = vec![
|
||||
(ParseAmountError::Negative, "amount is negative"),
|
||||
(ParseAmountError::TooBig, "amount is too large"),
|
||||
(ParseAmountError::TooPrecise, "amount is too precise"),
|
||||
(ParseAmountError::InvalidFormat, "invalid amount format"),
|
||||
(ParseAmountError::InputTooLarge, "input is too large"),
|
||||
(
|
||||
ParseAmountError::InvalidCharacter {
|
||||
error_message: "invalid char".to_string(),
|
||||
},
|
||||
"invalid character: invalid char",
|
||||
),
|
||||
(
|
||||
ParseAmountError::UnknownDenomination {
|
||||
error_message: "unknown denom".to_string(),
|
||||
},
|
||||
"unknown denomination: unknown denom",
|
||||
),
|
||||
(
|
||||
ParseAmountError::PossiblyConfusingDenomination {
|
||||
error_message: "confusing denom".to_string(),
|
||||
},
|
||||
"possibly confusing denomination: confusing denom",
|
||||
),
|
||||
(
|
||||
ParseAmountError::OtherParseAmountErr,
|
||||
"unknown parse amount error",
|
||||
),
|
||||
];
|
||||
|
||||
for (error, expected_message) in cases {
|
||||
assert_eq!(error.to_string(), expected_message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_persistence_error() {
|
||||
let cases = vec![
|
||||
@@ -1449,13 +1739,13 @@ mod test {
|
||||
PsbtParseError::PsbtEncoding {
|
||||
error_message: "invalid PSBT structure".to_string(),
|
||||
},
|
||||
"error in internal PSBT data structure: invalid PSBT structure",
|
||||
"error in internal psbt data structure: invalid PSBT structure",
|
||||
),
|
||||
(
|
||||
PsbtParseError::Base64Encoding {
|
||||
error_message: "base64 decode error".to_string(),
|
||||
},
|
||||
"error in PSBT base64 encoding: base64 decode error",
|
||||
"error in psbt base64 encoding: base64 decode error",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1467,46 +1757,46 @@ mod test {
|
||||
#[test]
|
||||
fn test_signer_errors() {
|
||||
let errors = vec![
|
||||
(SignerError::MissingKey, "Missing key for signing"),
|
||||
(SignerError::InvalidKey, "Invalid key provided"),
|
||||
(SignerError::UserCanceled, "User canceled operation"),
|
||||
(SignerError::MissingKey, "missing key for signing"),
|
||||
(SignerError::InvalidKey, "invalid key provided"),
|
||||
(SignerError::UserCanceled, "user canceled operation"),
|
||||
(
|
||||
SignerError::InputIndexOutOfRange,
|
||||
"Input index out of range",
|
||||
"input index out of range",
|
||||
),
|
||||
(
|
||||
SignerError::MissingNonWitnessUtxo,
|
||||
"Missing non-witness UTXO information",
|
||||
"missing non-witness utxo information",
|
||||
),
|
||||
(
|
||||
SignerError::InvalidNonWitnessUtxo,
|
||||
"Invalid non-witness UTXO information provided",
|
||||
"invalid non-witness utxo information provided",
|
||||
),
|
||||
(SignerError::MissingWitnessUtxo, "Missing witness UTXO"),
|
||||
(SignerError::MissingWitnessScript, "Missing witness script"),
|
||||
(SignerError::MissingHdKeypath, "Missing HD keypath"),
|
||||
(SignerError::MissingWitnessUtxo, "missing witness utxo"),
|
||||
(SignerError::MissingWitnessScript, "missing witness script"),
|
||||
(SignerError::MissingHdKeypath, "missing hd keypath"),
|
||||
(
|
||||
SignerError::NonStandardSighash,
|
||||
"Non-standard sighash type used",
|
||||
"non-standard sighash type used",
|
||||
),
|
||||
(SignerError::InvalidSighash, "Invalid sighash type provided"),
|
||||
(SignerError::InvalidSighash, "invalid sighash type provided"),
|
||||
(
|
||||
SignerError::SighashError {
|
||||
error_message: "dummy error".into(),
|
||||
},
|
||||
"Error with sighash computation: dummy error",
|
||||
"error with sighash computation: dummy error",
|
||||
),
|
||||
(
|
||||
SignerError::MiniscriptPsbt {
|
||||
error_message: "psbt issue".into(),
|
||||
},
|
||||
"Miniscript Psbt error: psbt issue",
|
||||
"miniscript psbt error: psbt issue",
|
||||
),
|
||||
(
|
||||
SignerError::External {
|
||||
error_message: "external error".into(),
|
||||
},
|
||||
"External error: external error",
|
||||
"external error: external error",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1518,7 +1808,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_error_transaction() {
|
||||
let cases = vec![
|
||||
(TransactionError::Io, "IO error"),
|
||||
(TransactionError::Io, "io error"),
|
||||
(
|
||||
TransactionError::OversizedVectorAllocation,
|
||||
"allocation of oversized vector",
|
||||
@@ -1530,7 +1820,7 @@ mod test {
|
||||
},
|
||||
"invalid checksum: expected=deadbeef actual=beadbeef",
|
||||
),
|
||||
(TransactionError::NonMinimalVarInt, "non-minimal varint"),
|
||||
(TransactionError::NonMinimalVarInt, "non-minimal var int"),
|
||||
(TransactionError::ParseFailed, "parse failed"),
|
||||
(
|
||||
TransactionError::UnsupportedSegwitFlag { flag: 1 },
|
||||
@@ -1605,6 +1895,13 @@ mod test {
|
||||
},
|
||||
"loaded network type is not bitcoin, got Some(Testnet)".to_string(),
|
||||
),
|
||||
(
|
||||
WalletCreationError::LoadedDescriptorDoesNotMatch {
|
||||
got: "def".to_string(),
|
||||
keychain: KeychainKind::External,
|
||||
},
|
||||
"loaded descriptor 'def' does not match what was provided 'External'".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
for (error, expected) in errors {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod bitcoin;
|
||||
mod descriptor;
|
||||
mod electrum;
|
||||
mod error;
|
||||
mod esplora;
|
||||
mod keys;
|
||||
@@ -7,13 +8,16 @@ mod types;
|
||||
mod wallet;
|
||||
|
||||
use crate::bitcoin::Address;
|
||||
use crate::bitcoin::Amount;
|
||||
use crate::bitcoin::FeeRate;
|
||||
use crate::bitcoin::OutPoint;
|
||||
use crate::bitcoin::Psbt;
|
||||
use crate::bitcoin::Script;
|
||||
use crate::bitcoin::Transaction;
|
||||
use crate::bitcoin::TxIn;
|
||||
use crate::bitcoin::TxOut;
|
||||
use crate::descriptor::Descriptor;
|
||||
use crate::electrum::ElectrumClient;
|
||||
use crate::error::AddressError;
|
||||
use crate::error::Bip32Error;
|
||||
use crate::error::Bip39Error;
|
||||
@@ -22,9 +26,11 @@ use crate::error::CannotConnectError;
|
||||
use crate::error::CreateTxError;
|
||||
use crate::error::DescriptorError;
|
||||
use crate::error::DescriptorKeyError;
|
||||
use crate::error::ElectrumError;
|
||||
use crate::error::EsploraError;
|
||||
use crate::error::ExtractTxError;
|
||||
use crate::error::FeeRateError;
|
||||
use crate::error::ParseAmountError;
|
||||
use crate::error::PersistenceError;
|
||||
use crate::error::PsbtParseError;
|
||||
use crate::error::SignerError;
|
||||
|
||||
@@ -11,6 +11,8 @@ use bdk::LocalOutput as BdkLocalOutput;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::bitcoin::Amount;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ChainPosition {
|
||||
Confirmed { height: u32, timestamp: u64 },
|
||||
@@ -45,7 +47,7 @@ impl From<BdkCanonicalTx<'_, Arc<bdk::bitcoin::Transaction>, ConfirmationTimeHei
|
||||
|
||||
pub struct ScriptAmount {
|
||||
pub script: Arc<Script>,
|
||||
pub amount: u64,
|
||||
pub amount: Arc<Amount>,
|
||||
}
|
||||
|
||||
pub struct AddressInfo {
|
||||
@@ -65,23 +67,23 @@ impl From<BdkAddressInfo> for AddressInfo {
|
||||
}
|
||||
|
||||
pub struct Balance {
|
||||
pub immature: u64,
|
||||
pub trusted_pending: u64,
|
||||
pub untrusted_pending: u64,
|
||||
pub confirmed: u64,
|
||||
pub trusted_spendable: u64,
|
||||
pub total: u64,
|
||||
pub immature: Arc<Amount>,
|
||||
pub trusted_pending: Arc<Amount>,
|
||||
pub untrusted_pending: Arc<Amount>,
|
||||
pub confirmed: Arc<Amount>,
|
||||
pub trusted_spendable: Arc<Amount>,
|
||||
pub total: Arc<Amount>,
|
||||
}
|
||||
|
||||
impl From<BdkBalance> for Balance {
|
||||
fn from(bdk_balance: BdkBalance) -> Self {
|
||||
Balance {
|
||||
immature: bdk_balance.immature,
|
||||
trusted_pending: bdk_balance.trusted_pending,
|
||||
untrusted_pending: bdk_balance.untrusted_pending,
|
||||
confirmed: bdk_balance.confirmed,
|
||||
trusted_spendable: bdk_balance.trusted_spendable(),
|
||||
total: bdk_balance.total(),
|
||||
immature: Arc::new(bdk_balance.immature.into()),
|
||||
trusted_pending: Arc::new(bdk_balance.trusted_pending.into()),
|
||||
untrusted_pending: Arc::new(bdk_balance.untrusted_pending.into()),
|
||||
confirmed: Arc::new(bdk_balance.confirmed.into()),
|
||||
trusted_spendable: Arc::new(bdk_balance.trusted_spendable().into()),
|
||||
total: Arc::new(bdk_balance.total().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::bitcoin::Amount;
|
||||
use crate::bitcoin::{FeeRate, OutPoint, Psbt, Script, Transaction};
|
||||
use crate::descriptor::Descriptor;
|
||||
use crate::error::{
|
||||
CalculateFeeError, CannotConnectError, CreateTxError, PersistenceError, SignerError,
|
||||
TxidParseError, WalletCreationError,
|
||||
CalculateFeeError, CannotConnectError, CreateTxError, DescriptorError, PersistenceError,
|
||||
SignerError, TxidParseError, WalletCreationError,
|
||||
};
|
||||
use crate::types::{
|
||||
AddressInfo, Balance, CanonicalTx, FullScanRequest, LocalOutput, ScriptAmount, SyncRequest,
|
||||
};
|
||||
|
||||
use bdk::bitcoin::amount::Amount as BdkAmount;
|
||||
use bdk::bitcoin::blockdata::script::ScriptBuf as BdkScriptBuf;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::bitcoin::Psbt as BdkPsbt;
|
||||
@@ -47,6 +49,22 @@ impl Wallet {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_no_persist(
|
||||
descriptor: Arc<Descriptor>,
|
||||
change_descriptor: Option<Arc<Descriptor>>,
|
||||
network: Network,
|
||||
) -> Result<Self, DescriptorError> {
|
||||
let descriptor = descriptor.as_string_private();
|
||||
let change_descriptor = change_descriptor.map(|d| d.as_string_private());
|
||||
|
||||
let wallet: BdkWallet =
|
||||
BdkWallet::new_no_persist(&descriptor, change_descriptor.as_ref(), network)?;
|
||||
|
||||
Ok(Wallet {
|
||||
inner_mutex: Mutex::new(wallet),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
|
||||
self.inner_mutex.lock().expect("wallet")
|
||||
}
|
||||
@@ -82,7 +100,7 @@ impl Wallet {
|
||||
}
|
||||
|
||||
pub fn get_balance(&self) -> Balance {
|
||||
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance();
|
||||
let bdk_balance = self.get_wallet().get_balance();
|
||||
Balance::from(bdk_balance)
|
||||
}
|
||||
|
||||
@@ -102,8 +120,11 @@ impl Wallet {
|
||||
}
|
||||
|
||||
pub fn sent_and_received(&self, tx: &Transaction) -> SentAndReceivedValues {
|
||||
let (sent, received): (u64, u64) = self.get_wallet().sent_and_received(&tx.into());
|
||||
SentAndReceivedValues { sent, received }
|
||||
let (sent, received) = self.get_wallet().sent_and_received(&tx.into());
|
||||
SentAndReceivedValues {
|
||||
sent: Arc::new(sent.into()),
|
||||
received: Arc::new(received.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transactions(&self) -> Vec<CanonicalTx> {
|
||||
@@ -152,15 +173,15 @@ impl Wallet {
|
||||
}
|
||||
|
||||
pub struct SentAndReceivedValues {
|
||||
pub sent: u64,
|
||||
pub received: u64,
|
||||
pub sent: Arc<Amount>,
|
||||
pub received: Arc<Amount>,
|
||||
}
|
||||
|
||||
pub struct Update(pub(crate) BdkUpdate);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TxBuilder {
|
||||
pub(crate) recipients: Vec<(BdkScriptBuf, u64)>,
|
||||
pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>,
|
||||
pub(crate) utxos: Vec<OutPoint>,
|
||||
pub(crate) unspendable: HashSet<OutPoint>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
@@ -190,9 +211,9 @@ impl TxBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_recipient(&self, script: &Script, amount: u64) -> Arc<Self> {
|
||||
let mut recipients: Vec<(BdkScriptBuf, u64)> = self.recipients.clone();
|
||||
recipients.append(&mut vec![(script.0.clone(), amount)]);
|
||||
pub(crate) fn add_recipient(&self, script: &Script, amount: Arc<Amount>) -> Arc<Self> {
|
||||
let mut recipients: Vec<(BdkScriptBuf, BdkAmount)> = self.recipients.clone();
|
||||
recipients.append(&mut vec![(script.0.clone(), amount.0)]);
|
||||
|
||||
Arc::new(TxBuilder {
|
||||
recipients,
|
||||
@@ -203,7 +224,7 @@ impl TxBuilder {
|
||||
pub(crate) fn set_recipients(&self, recipients: Vec<ScriptAmount>) -> Arc<Self> {
|
||||
let recipients = recipients
|
||||
.iter()
|
||||
.map(|script_amount| (script_amount.script.0.clone(), script_amount.amount))
|
||||
.map(|script_amount| (script_amount.script.0.clone(), script_amount.amount.0)) //;
|
||||
.collect();
|
||||
Arc::new(TxBuilder {
|
||||
recipients,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
android.enableJetifier=true
|
||||
kotlin.code.style=official
|
||||
libraryVersion=1.0.0-alpha.10-SNAPSHOT
|
||||
libraryVersion=1.0.0-alpha.11
|
||||
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,15 @@ class LiveTxBuilderTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
val psbt: Psbt = TxBuilder()
|
||||
.addRecipient(recipient.scriptPubkey(), 4200uL)
|
||||
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
|
||||
.feeRate(FeeRate.fromSatPerVb(2uL))
|
||||
.finish(wallet)
|
||||
|
||||
@@ -56,15 +58,17 @@ class LiveTxBuilderTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.SIGNET)
|
||||
val allRecipients: List<ScriptAmount> = listOf(
|
||||
ScriptAmount(recipient1.scriptPubkey(), 4200uL),
|
||||
ScriptAmount(recipient2.scriptPubkey(), 4200uL),
|
||||
ScriptAmount(recipient1.scriptPubkey(), Amount.fromSat(4200uL)),
|
||||
ScriptAmount(recipient2.scriptPubkey(), Amount.fromSat(4200uL)),
|
||||
)
|
||||
|
||||
val psbt: Psbt = TxBuilder()
|
||||
|
||||
@@ -31,9 +31,11 @@ class LiveWalletTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL)
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
println("Transactions count: ${wallet.transactions().count()}")
|
||||
val transactions = wallet.transactions().take(3)
|
||||
@@ -54,17 +56,16 @@ class LiveWalletTest {
|
||||
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
|
||||
wallet.applyUpdate(update)
|
||||
wallet.commit()
|
||||
println("Balance: ${wallet.getBalance().total}")
|
||||
println("New address: ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()}")
|
||||
println("Balance: ${wallet.getBalance().total.toSat()}")
|
||||
|
||||
assert(wallet.getBalance().total > 0uL) {
|
||||
assert(wallet.getBalance().total.toSat() > 0uL) {
|
||||
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
|
||||
}
|
||||
|
||||
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.SIGNET)
|
||||
|
||||
val psbt: Psbt = TxBuilder()
|
||||
.addRecipient(recipient.scriptPubkey(), 4200uL)
|
||||
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4200uL))
|
||||
.feeRate(FeeRate.fromSatPerVb(2uL))
|
||||
.finish(wallet)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class OfflineWalletTest {
|
||||
|
||||
assertEquals(
|
||||
expected = 0uL,
|
||||
actual = wallet.getBalance().total
|
||||
actual = wallet.getBalance().total.toSat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import bdkpython as bdk
|
||||
|
||||
setup(
|
||||
name="bdkpython",
|
||||
version="1.0.0a10.dev",
|
||||
version="1.0.0a11",
|
||||
description="The Python language bindings for the Bitcoin Development Kit",
|
||||
long_description=LONG_DESCRIPTION,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -13,7 +13,7 @@ class LiveTxBuilderTest(unittest.TestCase):
|
||||
|
||||
def test_tx_builder(self):
|
||||
descriptor: bdk.Descriptor = bdk.Descriptor(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
|
||||
bdk.Network.SIGNET
|
||||
)
|
||||
wallet: bdk.Wallet = bdk.Wallet(
|
||||
@@ -32,29 +32,29 @@ class LiveTxBuilderTest(unittest.TestCase):
|
||||
wallet.apply_update(update)
|
||||
wallet.commit()
|
||||
|
||||
self.assertGreater(wallet.get_balance().total, 0)
|
||||
self.assertGreater(
|
||||
wallet.get_balance().total.to_sat(),
|
||||
0,
|
||||
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
|
||||
)
|
||||
|
||||
recipient = bdk.Address(
|
||||
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
|
||||
network=bdk.Network.SIGNET
|
||||
)
|
||||
|
||||
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
|
||||
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
|
||||
|
||||
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
|
||||
|
||||
def complex_tx_builder(self):
|
||||
descriptor: bdk.Descriptor = bdk.Descriptor(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
bdk.Network.SIGNET
|
||||
)
|
||||
change_descriptor: bdk.Descriptor = bdk.Descriptor(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
|
||||
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
|
||||
bdk.Network.SIGNET
|
||||
)
|
||||
wallet: bdk.Wallet = bdk.Wallet(
|
||||
descriptor,
|
||||
change_descriptor,
|
||||
None,
|
||||
"./bdk_persistence.db",
|
||||
bdk.Network.SIGNET
|
||||
)
|
||||
@@ -68,7 +68,11 @@ class LiveTxBuilderTest(unittest.TestCase):
|
||||
wallet.apply_update(update)
|
||||
wallet.commit()
|
||||
|
||||
self.assertGreater(wallet.get_balance().total, 0)
|
||||
self.assertGreater(
|
||||
wallet.get_balance().total.to_sat(),
|
||||
0,
|
||||
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
|
||||
)
|
||||
|
||||
recipient1 = bdk.Address(
|
||||
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
|
||||
|
||||
@@ -32,15 +32,19 @@ class LiveWalletTest(unittest.TestCase):
|
||||
wallet.apply_update(update)
|
||||
wallet.commit()
|
||||
|
||||
self.assertGreater(wallet.get_balance().total, 0)
|
||||
self.assertGreater(
|
||||
wallet.get_balance().total.to_sat(),
|
||||
0,
|
||||
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
|
||||
)
|
||||
|
||||
print(f"Transactions count: {len(wallet.transactions())}")
|
||||
transactions = wallet.transactions()[:3]
|
||||
for tx in transactions:
|
||||
sent_and_received = wallet.sent_and_received(tx.transaction)
|
||||
print(f"Transaction: {tx.transaction.txid()}")
|
||||
print(f"Sent {sent_and_received.sent}")
|
||||
print(f"Received {sent_and_received.received}")
|
||||
print(f"Sent {sent_and_received.sent.to_sat()}")
|
||||
print(f"Received {sent_and_received.received.to_sat()}")
|
||||
|
||||
|
||||
def test_broadcast_transaction(self):
|
||||
@@ -64,15 +68,18 @@ class LiveWalletTest(unittest.TestCase):
|
||||
wallet.apply_update(update)
|
||||
wallet.commit()
|
||||
|
||||
self.assertGreater(wallet.get_balance().total, 0)
|
||||
self.assertGreater(
|
||||
wallet.get_balance().total.to_sat(),
|
||||
0,
|
||||
f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(bdk.KeychainKind.EXTERNAL).address.as_string()} and try again."
|
||||
)
|
||||
|
||||
recipient = bdk.Address(
|
||||
address="tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
|
||||
network=bdk.Network.SIGNET
|
||||
)
|
||||
|
||||
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
|
||||
# print(psbt.serialize())
|
||||
psbt: bdk.Psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=bdk.Amount.from_sat(4200)).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2)).finish(wallet)
|
||||
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
|
||||
|
||||
walletDidSign = wallet.sign(psbt)
|
||||
|
||||
@@ -40,7 +40,7 @@ class OfflineWalletTest(unittest.TestCase):
|
||||
bdk.Network.TESTNET
|
||||
)
|
||||
|
||||
self.assertEqual(wallet.get_balance().total, 0)
|
||||
self.assertEqual(wallet.get_balance().total.to_sat(), 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -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())")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import XCTest
|
||||
@testable import BitcoinDevKit
|
||||
|
||||
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
|
||||
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
|
||||
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
|
||||
final class LiveTxBuilderTests: XCTestCase {
|
||||
var dbFilePath: URL!
|
||||
@@ -45,13 +45,18 @@ final class LiveTxBuilderTests: XCTestCase {
|
||||
parallelRequests: 1
|
||||
)
|
||||
try wallet.applyUpdate(update: update)
|
||||
try wallet.commit()
|
||||
let _ = try wallet.commit()
|
||||
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
|
||||
|
||||
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
|
||||
XCTAssertGreaterThan(
|
||||
wallet.getBalance().total.toSat(),
|
||||
UInt64(0),
|
||||
"Wallet must have positive balance, please send funds to \(address)"
|
||||
)
|
||||
|
||||
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
|
||||
let psbt: Psbt = try TxBuilder()
|
||||
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
|
||||
.addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
|
||||
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
|
||||
.finish(wallet: wallet)
|
||||
|
||||
@@ -82,15 +87,20 @@ final class LiveTxBuilderTests: XCTestCase {
|
||||
parallelRequests: 1
|
||||
)
|
||||
try wallet.applyUpdate(update: update)
|
||||
try wallet.commit()
|
||||
|
||||
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
|
||||
let _ = try wallet.commit()
|
||||
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
wallet.getBalance().total.toSat(),
|
||||
UInt64(0),
|
||||
"Wallet must have positive balance, please send funds to \(address)"
|
||||
)
|
||||
|
||||
let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
|
||||
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .signet)
|
||||
let allRecipients: [ScriptAmount] = [
|
||||
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200),
|
||||
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200)
|
||||
ScriptAmount(script: recipient1.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200)),
|
||||
ScriptAmount(script: recipient2.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
|
||||
]
|
||||
|
||||
let psbt: Psbt = try TxBuilder()
|
||||
@@ -100,7 +110,7 @@ final class LiveTxBuilderTests: XCTestCase {
|
||||
.enableRbf()
|
||||
.finish(wallet: wallet)
|
||||
|
||||
try! wallet.sign(psbt: psbt)
|
||||
let _ = try! wallet.sign(psbt: psbt)
|
||||
|
||||
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import XCTest
|
||||
@testable import BitcoinDevKit
|
||||
|
||||
let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
|
||||
let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
|
||||
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
|
||||
|
||||
final class LiveWalletTests: XCTestCase {
|
||||
var dbFilePath: URL!
|
||||
@@ -45,17 +45,22 @@ final class LiveWalletTests: XCTestCase {
|
||||
parallelRequests: 1
|
||||
)
|
||||
try wallet.applyUpdate(update: update)
|
||||
try wallet.commit()
|
||||
let _ = try wallet.commit()
|
||||
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
|
||||
|
||||
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0))
|
||||
XCTAssertGreaterThan(
|
||||
wallet.getBalance().total.toSat(),
|
||||
UInt64(0),
|
||||
"Wallet must have positive balance, please send funds to \(address)"
|
||||
)
|
||||
|
||||
print("Transactions count: \(wallet.transactions().count)")
|
||||
let transactions = wallet.transactions().prefix(3)
|
||||
for tx in transactions {
|
||||
let sentAndReceived = wallet.sentAndReceived(tx: tx.transaction)
|
||||
print("Transaction: \(tx.transaction.txid())")
|
||||
print("Sent \(sentAndReceived.sent)")
|
||||
print("Received \(sentAndReceived.received)")
|
||||
print("Sent \(sentAndReceived.sent.toSat())")
|
||||
print("Received \(sentAndReceived.received.toSat())")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,16 +83,21 @@ final class LiveWalletTests: XCTestCase {
|
||||
parallelRequests: 1
|
||||
)
|
||||
try wallet.applyUpdate(update: update)
|
||||
try wallet.commit()
|
||||
|
||||
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
|
||||
let _ = try wallet.commit()
|
||||
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
wallet.getBalance().total.toSat(),
|
||||
UInt64(0),
|
||||
"Wallet must have positive balance, please send funds to \(address)"
|
||||
)
|
||||
|
||||
print("Balance: \(wallet.getBalance().total)")
|
||||
|
||||
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .signet)
|
||||
let psbt: Psbt = try
|
||||
TxBuilder()
|
||||
.addRecipient(script: recipient.scriptPubkey(), amount: 4200)
|
||||
.addRecipient(script: recipient.scriptPubkey(), amount: Amount.fromSat(fromSat: 4200))
|
||||
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2))
|
||||
.finish(wallet: wallet)
|
||||
|
||||
|
||||
@@ -60,6 +60,6 @@ final class OfflineWalletTests: XCTestCase {
|
||||
network: .testnet
|
||||
)
|
||||
|
||||
XCTAssertEqual(wallet.getBalance().total, 0)
|
||||
XCTAssertEqual(wallet.getBalance().total.toSat(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user