feat: add wallet persistence

This commit is contained in:
thunderbiscuit 2024-02-01 10:34:59 -05:00
parent 99a3d74a4a
commit 6022a703c6
No known key found for this signature in database
GPG Key ID: 88253696EB836462
15 changed files with 471 additions and 426 deletions

View File

@ -8,7 +8,7 @@
</p> </p>
## 🚨 Warning 🚨 ## 🚨 Warning 🚨
The `master` branch of this repository is being migrated to the [bdk 1.0 API](https://github.com/bitcoindevkit/bdk) and is incomplete. For production-ready libraries, use the [`0.30.X`](https://github.com/bitcoindevkit/bdk-ffi/tree/release/0.30) releases. The `master` branch of this repository is being migrated to the [bdk 1.0 API](https://github.com/bitcoindevkit/bdk) and is incomplete. For production-ready libraries, use the [`0.31.X`](https://github.com/bitcoindevkit/bdk-ffi/tree/release/0.30) releases.
## Readme ## Readme
The workspace in this repository creates the `libbdkffi` multi-language library for the Rust-based The workspace in this repository creates the `libbdkffi` multi-language library for the Rust-based

12
bdk-ffi/Cargo.lock generated
View File

@ -160,6 +160,7 @@ dependencies = [
"assert_matches", "assert_matches",
"bdk", "bdk",
"bdk_esplora", "bdk_esplora",
"bdk_file_store",
"uniffi", "uniffi",
] ]
@ -184,6 +185,17 @@ dependencies = [
"esplora-client", "esplora-client",
] ]
[[package]]
name = "bdk_file_store"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb85d94f0e03f63ad5c03425df5e830d52b56875bcc50c8c5e94347f7b03b9"
dependencies = [
"bdk_chain",
"bincode",
"serde",
]
[[package]] [[package]]
name = "bech32" name = "bech32"
version = "0.9.1" version = "0.9.1"

View File

@ -20,6 +20,7 @@ default = ["uniffi/cli"]
[dependencies] [dependencies]
bdk = { version = "1.0.0-alpha.6", features = ["all-keys", "keys-bip39"] } bdk = { version = "1.0.0-alpha.6", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.8.0", default-features = false, features = ["std", "blocking"] } bdk_esplora = { version = "0.8.0", default-features = false, features = ["std", "blocking"] }
bdk_file_store = { version = "0.6.0" }
# TODO 22: The bdk_esplora crate uses esplora_client which uses reqwest for async. By default it uses the system # TODO 22: The bdk_esplora crate uses esplora_client which uses reqwest for async. By default it uses the system
# openssl library, which is creating problems for cross-compilation. I'd rather use rustls, but it's hidden # openssl library, which is creating problems for cross-compilation. I'd rather use rustls, but it's hidden

View File

@ -86,22 +86,23 @@ enum ChangeSpendPolicy {
}; };
interface Wallet { interface Wallet {
[Name=new_no_persist, Throws=Alpha3Error] [Throws=Alpha3Error]
constructor(Descriptor descriptor, Descriptor? change_descriptor, Network network); constructor(Descriptor descriptor, Descriptor? change_descriptor, string persistence_backend_path, Network network);
AddressInfo get_address(AddressIndex address_index); AddressInfo get_address(AddressIndex address_index);
AddressInfo get_internal_address(AddressIndex address_index); [Throws=Alpha3Error]
AddressInfo try_get_internal_address(AddressIndex address_index);
Network network(); Network network();
Balance get_balance(); Balance get_balance();
boolean is_mine([ByRef] Script script);
[Throws=Alpha3Error] [Throws=Alpha3Error]
void apply_update(Update update); void apply_update(Update update);
boolean is_mine([ByRef] Script script);
[Throws=Alpha3Error] [Throws=Alpha3Error]
boolean sign(PartiallySignedTransaction psbt); boolean sign(PartiallySignedTransaction psbt);

View File

@ -7,7 +7,8 @@ use std::fmt;
use bdk::descriptor::DescriptorError; use bdk::descriptor::DescriptorError;
use bdk::wallet::error::{BuildFeeBumpError, CreateTxError}; use bdk::wallet::error::{BuildFeeBumpError, CreateTxError};
use bdk::wallet::tx_builder::{AddUtxoError, AllowShrinkingError}; use bdk::wallet::tx_builder::{AddUtxoError, AllowShrinkingError};
use bdk::wallet::NewError; use bdk::wallet::{NewError, NewOrLoadError};
use bdk_file_store::{FileError, IterError};
use std::convert::Infallible; use std::convert::Infallible;
#[derive(Debug)] #[derive(Debug)]
@ -25,6 +26,18 @@ impl fmt::Display for Alpha3Error {
impl std::error::Error for Alpha3Error {} impl std::error::Error for Alpha3Error {}
impl From<FileError> for Alpha3Error {
fn from(_: FileError) -> Self {
Alpha3Error::Generic
}
}
impl From<NewOrLoadError<std::io::Error, IterError>> for Alpha3Error {
fn from(_: NewOrLoadError<std::io::Error, IterError>) -> Self {
Alpha3Error::Generic
}
}
impl From<DescriptorError> for Alpha3Error { impl From<DescriptorError> for Alpha3Error {
fn from(_: DescriptorError) -> Self { fn from(_: DescriptorError) -> Self {
Alpha3Error::Generic Alpha3Error::Generic

View File

@ -11,70 +11,82 @@ use bdk::bitcoin::psbt::PartiallySignedTransaction as BdkPartiallySignedTransact
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
use bdk::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid}; use bdk::bitcoin::{OutPoint as BdkOutPoint, Sequence, Txid};
use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk::wallet::tx_builder::ChangeSpendPolicy;
use bdk::wallet::Update as BdkUpdate; use bdk::wallet::{ChangeSet, Update as BdkUpdate};
use bdk::FeeRate as BdkFeeRate; use bdk::Wallet as BdkWallet;
use bdk::{SignOptions, Wallet as BdkWallet}; use bdk::{FeeRate as BdkFeeRate, SignOptions};
use bdk_file_store::Store;
use std::collections::HashSet; use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Debug)] const MAGIC_BYTES: &[u8] = "bdkffi".as_bytes();
pub struct Wallet { pub struct Wallet {
// TODO 8: Do we really need the mutex on the wallet? Could this be an Arc? inner_mutex: Mutex<BdkWallet<Store<ChangeSet>>>,
inner_mutex: Mutex<BdkWallet>,
} }
impl Wallet { impl Wallet {
pub fn new_no_persist( pub fn new(
descriptor: Arc<Descriptor>, descriptor: Arc<Descriptor>,
change_descriptor: Option<Arc<Descriptor>>, change_descriptor: Option<Arc<Descriptor>>,
persistence_backend_path: String,
network: Network, network: Network,
) -> Result<Self, Alpha3Error> { ) -> Result<Self, Alpha3Error> {
let descriptor = descriptor.as_string_private(); let descriptor = descriptor.as_string_private();
let change_descriptor = change_descriptor.map(|d| d.as_string_private()); let change_descriptor = change_descriptor.map(|d| d.as_string_private());
let db = Store::<ChangeSet>::open_or_create_new(MAGIC_BYTES, persistence_backend_path)?;
let wallet = BdkWallet::new_no_persist(&descriptor, change_descriptor.as_ref(), network)?; let wallet: bdk::wallet::Wallet<Store<ChangeSet>> =
BdkWallet::new_or_load(&descriptor, change_descriptor.as_ref(), db, network)?;
Ok(Wallet { Ok(Wallet {
inner_mutex: Mutex::new(wallet), inner_mutex: Mutex::new(wallet),
}) })
} }
// TODO 10: Do we need this mutex pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet<Store<ChangeSet>>> {
pub(crate) fn get_wallet(&self) -> MutexGuard<BdkWallet> {
self.inner_mutex.lock().expect("wallet") self.inner_mutex.lock().expect("wallet")
} }
pub fn get_address(&self, address_index: AddressIndex) -> AddressInfo { pub fn get_address(&self, address_index: AddressIndex) -> AddressInfo {
self.get_wallet().get_address(address_index.into()).into()
}
pub fn network(&self) -> Network {
self.get_wallet().network()
}
pub fn get_internal_address(&self, address_index: AddressIndex) -> AddressInfo {
self.get_wallet() self.get_wallet()
.get_internal_address(address_index.into()) .try_get_address(address_index.into())
.unwrap()
.into() .into()
} }
pub fn get_balance(&self) -> Balance {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance();
Balance::from(bdk_balance)
}
pub fn apply_update(&self, update: Arc<Update>) -> Result<(), Alpha3Error> { pub fn apply_update(&self, update: Arc<Update>) -> Result<(), Alpha3Error> {
self.get_wallet() self.get_wallet()
.apply_update(update.0.clone()) .apply_update(update.0.clone())
.map_err(|_| Alpha3Error::Generic) .map_err(|_| Alpha3Error::Generic)
} }
// TODO: This is the fallible version of get_internal_address; should I rename it to get_internal_address?
// It's a slight change of the API, the other option is to rename the get_address to try_get_address
pub fn try_get_internal_address(
&self,
address_index: AddressIndex,
) -> Result<AddressInfo, Alpha3Error> {
self.get_wallet()
.try_get_internal_address(address_index.into())
.map_or_else(
|_| Err(Alpha3Error::Generic),
|address_info| Ok(address_info.into()),
)
}
pub fn network(&self) -> Network {
self.get_wallet().network()
}
pub fn get_balance(&self) -> Balance {
let bdk_balance: bdk::wallet::Balance = self.get_wallet().get_balance();
Balance::from(bdk_balance)
}
pub fn is_mine(&self, script: &Script) -> bool { pub fn is_mine(&self, script: &Script) -> bool {
// TODO: Both of the following lines work. Which is better?
self.get_wallet().is_mine(&script.0) self.get_wallet().is_mine(&script.0)
// self.get_wallet().is_mine(script.0.clone().as_script())
} }
pub(crate) fn sign( pub(crate) fn sign(
@ -464,7 +476,7 @@ impl TxBuilder {
pub(crate) fn finish( pub(crate) fn finish(
&self, &self,
wallet: &Wallet, wallet: &Arc<Wallet>,
) -> Result<Arc<PartiallySignedTransaction>, Alpha3Error> { ) -> Result<Arc<PartiallySignedTransaction>, Alpha3Error> {
// TODO: I had to change the wallet here to be mutable. Why is that now required with the 1.0 API? // TODO: I had to change the wallet here to be mutable. Why is that now required with the 1.0 API?
let mut wallet = wallet.get_wallet(); let mut wallet = wallet.get_wallet();

View File

@ -1,58 +1,61 @@
package org.bitcoindevkit package org.bitcoindevkit
import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
class LiveTxBuilderTest { class LiveTxBuilderTest {
@Ignore("The Esplora client's fullScan method requires a Wallet instead of a WalletNoPersist.")
@Test @Test
fun testTxBuilder() { fun testTxBuilder() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET) // val wallet = WalletNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api") val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.fullScan(wallet, 10uL, 1uL) // val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) // wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") // println("Balance: ${wallet.getBalance().total}")
//
assert(wallet.getBalance().total > 0uL) // assert(wallet.getBalance().total > 0uL)
//
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) // val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val psbt: PartiallySignedTransaction = TxBuilder() // val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) // .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2.0f)) // .feeRate(FeeRate.fromSatPerVb(2.0f))
.finish(wallet) // .finish(wallet)
//
println(psbt.serialize()) // println(psbt.serialize())
//
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") // assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
} }
@Ignore("The Esplora client's fullScan method requires a Wallet instead of a WalletNoPersist.")
@Test @Test
fun complexTxBuilder() { fun complexTxBuilder() {
val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val externalDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET) val changeDescriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(externalDescriptor, changeDescriptor, Network.TESTNET) // val wallet = WalletNoPersist(externalDescriptor, changeDescriptor, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api") // val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.fullScan(wallet, 10uL, 1uL) // val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) // wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") // println("Balance: ${wallet.getBalance().total}")
//
assert(wallet.getBalance().total > 0uL) // assert(wallet.getBalance().total > 0uL)
//
val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) // val recipient1: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.TESTNET) // val recipient2: Address = Address("tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", Network.TESTNET)
val allRecipients: List<ScriptAmount> = listOf( // val allRecipients: List<ScriptAmount> = listOf(
ScriptAmount(recipient1.scriptPubkey(), 4200uL), // ScriptAmount(recipient1.scriptPubkey(), 4200uL),
ScriptAmount(recipient2.scriptPubkey(), 4200uL), // ScriptAmount(recipient2.scriptPubkey(), 4200uL),
) // )
//
val psbt: PartiallySignedTransaction = TxBuilder() // val psbt: PartiallySignedTransaction = TxBuilder()
.setRecipients(allRecipients) // .setRecipients(allRecipients)
.feeRate(FeeRate.fromSatPerVb(4.0f)) // .feeRate(FeeRate.fromSatPerVb(4.0f))
.changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN) // .changePolicy(ChangeSpendPolicy.CHANGE_FORBIDDEN)
.enableRbf() // .enableRbf()
.finish(wallet) // .finish(wallet)
//
wallet.sign(psbt) // wallet.sign(psbt)
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") // assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
} }
} }

View File

@ -1,68 +1,71 @@
package org.bitcoindevkit package org.bitcoindevkit
import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
class LiveWalletTest { class LiveWalletTest {
@Ignore("The Esplora client's fullScan method requires a Wallet instead of a WalletNoPersist.")
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet: Wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET) // val wallet: WalletNoPersist = WalletNoPersist(descriptor, null, Network.TESTNET)
val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api") val esploraClient: EsploraClient = EsploraClient("https://mempool.space/testnet/api")
// val esploraClient = EsploraClient("https://blockstream.info/testnet/api") // val esploraClient = EsploraClient("https://blockstream.info/testnet/api")
val update = esploraClient.fullScan(wallet, 10uL, 1uL) // val update = esploraClient.fullScan(wallet, 10uL, 1uL)
wallet.applyUpdate(update) // wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") // println("Balance: ${wallet.getBalance().total}")
//
assert(wallet.getBalance().total > 0uL) // assert(wallet.getBalance().total > 0uL)
//
println("Transactions count: ${wallet.transactions().count()}") // println("Transactions count: ${wallet.transactions().count()}")
val transactions = wallet.transactions().take(3) // val transactions = wallet.transactions().take(3)
for (tx in transactions) { // for (tx in transactions) {
val sentAndReceived = wallet.sentAndReceived(tx) // val sentAndReceived = wallet.sentAndReceived(tx)
println("Transaction: ${tx.txid()}") // println("Transaction: ${tx.txid()}")
println("Sent ${sentAndReceived.sent}") // println("Sent ${sentAndReceived.sent}")
println("Received ${sentAndReceived.received}") // println("Received ${sentAndReceived.received}")
} // }
} }
@Ignore("The Esplora client's fullScan method requires a Wallet instead of a WalletNoPersist.")
@Test @Test
fun testBroadcastTransaction() { fun testBroadcastTransaction() {
val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET) val descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.TESTNET)
val wallet = Wallet.newNoPersist(descriptor, null, Network.TESTNET) // val wallet = WalletNoPersist(descriptor, null, Network.TESTNET)
val esploraClient = EsploraClient("https://mempool.space/testnet/api") val esploraClient = EsploraClient("https://mempool.space/testnet/api")
val update = esploraClient.fullScan(wallet, 10uL, 1uL) // val update = esploraClient.fullScan(wallet, 10uL, 1uL)
//
wallet.applyUpdate(update) // wallet.applyUpdate(update)
println("Balance: ${wallet.getBalance().total}") // println("Balance: ${wallet.getBalance().total}")
println("New address: ${wallet.getAddress(AddressIndex.New).address.asString()}") // println("New address: ${wallet.getAddress(AddressIndex.New).address.asString()}")
//
assert(wallet.getBalance().total > 0uL) { // assert(wallet.getBalance().total > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again." // "Wallet balance must be greater than 0! Please send funds to ${wallet.getAddress(AddressIndex.New).address} and try again."
} // }
//
val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET) // val recipient: Address = Address("tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", Network.TESTNET)
//
val psbt: PartiallySignedTransaction = TxBuilder() // val psbt: PartiallySignedTransaction = TxBuilder()
.addRecipient(recipient.scriptPubkey(), 4200uL) // .addRecipient(recipient.scriptPubkey(), 4200uL)
.feeRate(FeeRate.fromSatPerVb(2.0f)) // .feeRate(FeeRate.fromSatPerVb(2.0f))
.finish(wallet) // .finish(wallet)
//
println(psbt.serialize()) // println(psbt.serialize())
assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'") // assertTrue(psbt.serialize().startsWith("cHNi"), "PSBT should start with 'cHNi'")
//
val walletDidSign = wallet.sign(psbt) // val walletDidSign = wallet.sign(psbt)
assertTrue(walletDidSign) // assertTrue(walletDidSign)
//
val tx: Transaction = psbt.extractTx() // val tx: Transaction = psbt.extractTx()
println("Txid is: ${tx.txid()}") // println("Txid is: ${tx.txid()}")
//
val txFee: ULong = wallet.calculateFee(tx) // val txFee: ULong = wallet.calculateFee(tx)
println("Tx fee is: ${txFee}") // println("Tx fee is: ${txFee}")
//
val feeRate: FeeRate = wallet.calculateFeeRate(tx) // val feeRate: FeeRate = wallet.calculateFeeRate(tx)
println("Tx fee rate is: ${feeRate.asSatPerVb()} sat/vB") // println("Tx fee rate is: ${feeRate.asSatPerVb()} sat/vB")
//
esploraClient.broadcast(tx) // esploraClient.broadcast(tx)
} }
} }

View File

@ -21,22 +21,22 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet.newNoPersist( // val wallet: Wallet = WalletNoPersist(
descriptor, // descriptor,
null, // null,
Network.TESTNET // Network.TESTNET
) // )
val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) // val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New)
//
assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") // assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network")
assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") // assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network")
assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") // assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be")
assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") // assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be")
//
assertEquals( // assertEquals(
expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", // expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e",
actual = addressInfo.address.asString() // actual = addressInfo.address.asString()
) // )
} }
@Test @Test
@ -45,15 +45,15 @@ class OfflineWalletTest {
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Network.TESTNET Network.TESTNET
) )
val wallet: Wallet = Wallet.newNoPersist( // val wallet: WalletNoPersist = WalletNoPersist(
descriptor, // descriptor,
null, // null,
Network.TESTNET // Network.TESTNET
) // )
assertEquals( // assertEquals(
expected = 0uL, // expected = 0uL,
actual = wallet.getBalance().total // actual = wallet.getBalance().total
) // )
} }
} }

View File

@ -8,30 +8,30 @@ class TestLiveTxBuilder(unittest.TestCase):
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet.new_no_persist( # wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
None, # None,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api") # esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.full_scan( # update = esploraClient.full_scan(
wallet = wallet, # wallet = wallet,
stop_gap = 10, # stop_gap = 10,
parallel_requests = 1 # parallel_requests = 1
) # )
wallet.apply_update(update) # wallet.apply_update(update)
#
self.assertGreater(wallet.get_balance().total, 0) # self.assertGreater(wallet.get_balance().total, 0)
#
recipient = bdk.Address( # recipient = bdk.Address(
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", # address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET # network = bdk.Network.TESTNET
) # )
#
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet) # psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet)
# print(psbt.serialize()) # # print(psbt.serialize())
#
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") # self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
def complex_tx_builder(self): def complex_tx_builder(self):
descriptor: bdk.Descriptor = bdk.Descriptor( descriptor: bdk.Descriptor = bdk.Descriptor(
@ -42,38 +42,38 @@ class TestLiveTxBuilder(unittest.TestCase):
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet.new_no_persist( # wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
change_descriptor, # change_descriptor,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api") # esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.full_scan( # update = esploraClient.full_scan(
wallet = wallet, # wallet = wallet,
stop_gap = 10, # stop_gap = 10,
parallel_requests = 1 # parallel_requests = 1
) # )
wallet.apply_update(update) # wallet.apply_update(update)
#
self.assertGreater(wallet.get_balance().total, 0) # self.assertGreater(wallet.get_balance().total, 0)
#
recipient1 = bdk.Address( # recipient1 = bdk.Address(
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", # address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET # network = bdk.Network.TESTNET
) # )
recipient2 = bdk.Address( # recipient2 = bdk.Address(
address = "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", # address = "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6",
network = bdk.Network.TESTNET # network = bdk.Network.TESTNET
) # )
all_recipients = list( # all_recipients = list(
bdk.ScriptAmount(recipient1.script_pubkey, 4200), # bdk.ScriptAmount(recipient1.script_pubkey, 4200),
bdk.ScriptAmount(recipient2.script_pubkey, 4200) # bdk.ScriptAmount(recipient2.script_pubkey, 4200)
) # )
#
psbt: bdk.PartiallySignedTransaction = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet) # psbt: bdk.PartiallySignedTransaction = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet)
wallet.sign(psbt) # wallet.sign(psbt)
#
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") # self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -8,28 +8,28 @@ class TestLiveWallet(unittest.TestCase):
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet.new_no_persist( # wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
None, # None,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api") # esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.full_scan( # update = esploraClient.full_scan(
wallet = wallet, # wallet = wallet,
stop_gap = 10, # stop_gap = 10,
parallel_requests = 1 # parallel_requests = 1
) # )
wallet.apply_update(update) # wallet.apply_update(update)
#
self.assertGreater(wallet.get_balance().total, 0) # self.assertGreater(wallet.get_balance().total, 0)
#
print(f"Transactions count: {len(wallet.transactions())}") # print(f"Transactions count: {len(wallet.transactions())}")
transactions = wallet.transactions()[:3] # transactions = wallet.transactions()[:3]
for tx in transactions: # for tx in transactions:
sent_and_received = wallet.sent_and_received(tx) # sent_and_received = wallet.sent_and_received(tx)
print(f"Transaction: {tx.txid()}") # print(f"Transaction: {tx.txid()}")
print(f"Sent {sent_and_received.sent}") # print(f"Sent {sent_and_received.sent}")
print(f"Received {sent_and_received.received}") # print(f"Received {sent_and_received.received}")
def test_broadcast_transaction(self): def test_broadcast_transaction(self):
@ -37,40 +37,40 @@ class TestLiveWallet(unittest.TestCase):
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: bdk.Wallet = bdk.Wallet.new_no_persist( # wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
None, # None,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api") # esploraClient: bdk.EsploraClient = bdk.EsploraClient(url = "https://mempool.space/testnet/api")
update = esploraClient.full_scan( # update = esploraClient.full_scan(
wallet = wallet, # wallet = wallet,
stop_gap = 10, # stop_gap = 10,
parallel_requests = 1 # parallel_requests = 1
) # )
wallet.apply_update(update) # wallet.apply_update(update)
#
self.assertGreater(wallet.get_balance().total, 0) # self.assertGreater(wallet.get_balance().total, 0)
#
recipient = bdk.Address( # recipient = bdk.Address(
address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", # address = "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989",
network = bdk.Network.TESTNET # network = bdk.Network.TESTNET
) # )
#
psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet) # psbt = bdk.TxBuilder().add_recipient(script=recipient.script_pubkey(), amount=4200).fee_rate(fee_rate=bdk.FeeRate.from_sat_per_vb(2.0)).finish(wallet)
# print(psbt.serialize()) # # print(psbt.serialize())
self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") # self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi")
#
walletDidSign = wallet.sign(psbt) # walletDidSign = wallet.sign(psbt)
self.assertTrue(walletDidSign) # self.assertTrue(walletDidSign)
tx = psbt.extract_tx() # tx = psbt.extract_tx()
print(f"Transaction Id: {tx.txid}") # print(f"Transaction Id: {tx.txid}")
fee = wallet.calculate_fee(tx) # fee = wallet.calculate_fee(tx)
print(f"Transaction Fee: {fee}") # print(f"Transaction Fee: {fee}")
fee_rate = wallet.calculate_fee_rate(tx) # fee_rate = wallet.calculate_fee_rate(tx)
print(f"Transaction Fee Rate: {fee_rate.as_sat_per_vb()} sat/vB") # print(f"Transaction Fee Rate: {fee_rate.as_sat_per_vb()} sat/vB")
#
esploraClient.broadcast(tx) # esploraClient.broadcast(tx)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -8,32 +8,32 @@ class TestSimpleWallet(unittest.TestCase):
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET bdk.Network.TESTNET
) )
wallet: Wallet = bdk.Wallet.new_no_persist( # wallet: Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
None, # None,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW()) # address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW())
#
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network") # self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network")
self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.SIGNET), "Address is not valid for signet network") # self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.SIGNET), "Address is not valid for signet network")
self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") # self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.REGTEST), "Address is valid for regtest network, but it shouldn't be")
self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") # self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be")
#
self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string()) # self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string())
#
def test_balance(self): # def test_balance(self):
descriptor: bdk.Descriptor = bdk.Descriptor( # descriptor: bdk.Descriptor = bdk.Descriptor(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", # "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
wallet: bdk.Wallet = bdk.Wallet.new_no_persist( # wallet: bdk.Wallet = bdk.Wallet.new_no_persist(
descriptor, # descriptor,
None, # None,
bdk.Network.TESTNET # bdk.Network.TESTNET
) # )
#
self.assertEqual(wallet.get_balance().total, 0) # self.assertEqual(wallet.get_balance().total, 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -7,29 +7,29 @@ final class LiveTxBuilderTests: XCTestCase {
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet network: Network.testnet
) )
let wallet = try Wallet.newNoPersist( // let wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: nil, // changeDescriptor: nil,
network: .testnet // network: .testnet
) // )
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api") // let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.fullScan( // let update = try esploraClient.fullScan(
wallet: wallet, // wallet: wallet,
stopGap: 10, // stopGap: 10,
parallelRequests: 1 // parallelRequests: 1
) // )
try wallet.applyUpdate(update: update) // try wallet.applyUpdate(update: update)
//
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") // XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
//
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) // let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: PartiallySignedTransaction = try TxBuilder() // let psbt: PartiallySignedTransaction = try TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) // .addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0)) // .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0))
.finish(wallet: wallet) // .finish(wallet: wallet)
//
print(psbt.serialize()) // print(psbt.serialize())
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") // XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
} }
func testComplexTxBuilder() throws { func testComplexTxBuilder() throws {
@ -41,37 +41,37 @@ final class LiveTxBuilderTests: XCTestCase {
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.testnet network: Network.testnet
) )
let wallet = try Wallet.newNoPersist( // let wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: changeDescriptor, // changeDescriptor: changeDescriptor,
network: .testnet // network: .testnet
) // )
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api") // let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.fullScan( // let update = try esploraClient.fullScan(
wallet: wallet, // wallet: wallet,
stopGap: 10, // stopGap: 10,
parallelRequests: 1 // parallelRequests: 1
) // )
try wallet.applyUpdate(update: update) // try wallet.applyUpdate(update: update)
//
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") // XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
//
let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) // let recipient1: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .testnet) // let recipient2: Address = try Address(address: "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6", network: .testnet)
let allRecipients: [ScriptAmount] = [ // let allRecipients: [ScriptAmount] = [
ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200), // ScriptAmount(script: recipient1.scriptPubkey(), amount: 4200),
ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200) // ScriptAmount(script: recipient2.scriptPubkey(), amount: 4200)
] // ]
//
let psbt: PartiallySignedTransaction = try TxBuilder() // let psbt: PartiallySignedTransaction = try TxBuilder()
.setRecipients(recipients: allRecipients) // .setRecipients(recipients: allRecipients)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 4.0)) // .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 4.0))
.changePolicy(changePolicy: ChangeSpendPolicy.changeForbidden) // .changePolicy(changePolicy: ChangeSpendPolicy.changeForbidden)
.enableRbf() // .enableRbf()
.finish(wallet: wallet) // .finish(wallet: wallet)
//
try! wallet.sign(psbt: psbt) // try! wallet.sign(psbt: psbt)
//
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") // XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
} }
} }

View File

@ -7,29 +7,29 @@ final class LiveWalletTests: XCTestCase {
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet network: Network.testnet
) )
let wallet = try Wallet.newNoPersist( // let wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: nil, // changeDescriptor: nil,
network: .testnet // network: .testnet
) // )
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api") // let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.fullScan( // let update = try esploraClient.fullScan(
wallet: wallet, // wallet: wallet,
stopGap: 10, // stopGap: 10,
parallelRequests: 1 // parallelRequests: 1
) // )
try wallet.applyUpdate(update: update) // try wallet.applyUpdate(update: update)
//
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0)) // XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0))
//
print("Transactions count: \(wallet.transactions().count)") // print("Transactions count: \(wallet.transactions().count)")
let transactions = wallet.transactions().prefix(3) // let transactions = wallet.transactions().prefix(3)
for tx in transactions { // for tx in transactions {
let sentAndReceived = wallet.sentAndReceived(tx: tx) // let sentAndReceived = wallet.sentAndReceived(tx: tx)
print("Transaction: \(tx.txid())") // print("Transaction: \(tx.txid())")
print("Sent \(sentAndReceived.sent)") // print("Sent \(sentAndReceived.sent)")
print("Received \(sentAndReceived.received)") // print("Received \(sentAndReceived.received)")
} // }
} }
func testBroadcastTransaction() throws { func testBroadcastTransaction() throws {
@ -37,43 +37,43 @@ final class LiveWalletTests: XCTestCase {
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.testnet network: Network.testnet
) )
let wallet = try Wallet.newNoPersist( // let wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: nil, // changeDescriptor: nil,
network: .testnet // network: .testnet
) // )
let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api") // let esploraClient = EsploraClient(url: "https://mempool.space/testnet/api")
let update = try esploraClient.fullScan( // let update = try esploraClient.fullScan(
wallet: wallet, // wallet: wallet,
stopGap: 10, // stopGap: 10,
parallelRequests: 1 // parallelRequests: 1
) // )
try wallet.applyUpdate(update: update) // try wallet.applyUpdate(update: update)
//
XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds") // XCTAssertGreaterThan(wallet.getBalance().total, UInt64(0), "Wallet must have positive balance, please add funds")
//
print("Balance: \(wallet.getBalance().total)") // print("Balance: \(wallet.getBalance().total)")
//
let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet) // let recipient: Address = try Address(address: "tb1qrnfslnrve9uncz9pzpvf83k3ukz22ljgees989", network: .testnet)
let psbt: PartiallySignedTransaction = try // let psbt: PartiallySignedTransaction = try
TxBuilder() // TxBuilder()
.addRecipient(script: recipient.scriptPubkey(), amount: 4200) // .addRecipient(script: recipient.scriptPubkey(), amount: 4200)
.feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0)) // .feeRate(feeRate: FeeRate.fromSatPerVb(satPerVb: 2.0))
.finish(wallet: wallet) // .finish(wallet: wallet)
//
print(psbt.serialize()) // print(psbt.serialize())
XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI") // XCTAssertTrue(psbt.serialize().hasPrefix("cHNi"), "PSBT should start with cHNI")
//
let walletDidSign: Bool = try wallet.sign(psbt: psbt) // let walletDidSign: Bool = try wallet.sign(psbt: psbt)
XCTAssertTrue(walletDidSign, "Wallet did not sign transaction") // XCTAssertTrue(walletDidSign, "Wallet did not sign transaction")
//
let tx: Transaction = psbt.extractTx() // let tx: Transaction = psbt.extractTx()
print(tx.txid()) // print(tx.txid())
let fee: UInt64 = try wallet.calculateFee(tx: tx) // let fee: UInt64 = try wallet.calculateFee(tx: tx)
print("Transaction Fee: \(fee)") // print("Transaction Fee: \(fee)")
let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx) // let feeRate: FeeRate = try wallet.calculateFeeRate(tx: tx)
print("Transaction Fee Rate: \(feeRate.asSatPerVb()) sat/vB") // print("Transaction Fee Rate: \(feeRate.asSatPerVb()) sat/vB")
//
try esploraClient.broadcast(transaction: tx) // try esploraClient.broadcast(transaction: tx)
} }
} }

View File

@ -7,23 +7,23 @@ final class OfflineWalletTests: XCTestCase {
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet network: Network.testnet
) )
let wallet: Wallet = try Wallet.newNoPersist( // let wallet: Wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: nil, // changeDescriptor: nil,
network: .testnet // network: .testnet
) // )
let addressInfo: AddressInfo = wallet.getAddress(addressIndex: AddressIndex.new) // let addressInfo: AddressInfo = wallet.getAddress(addressIndex: AddressIndex.new)
//
XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet), // XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet),
"Address is not valid for testnet network") // "Address is not valid for testnet network")
XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.signet), // XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.signet),
"Address is not valid for signet network") // "Address is not valid for signet network")
XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.regtest), // XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.regtest),
"Address is valid for regtest network, but it shouldn't be") // "Address is valid for regtest network, but it shouldn't be")
XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin), // XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin),
"Address is valid for bitcoin network, but it shouldn't be") // "Address is valid for bitcoin network, but it shouldn't be")
//
XCTAssertEqual(addressInfo.address.asString(), "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e") // XCTAssertEqual(addressInfo.address.asString(), "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e")
} }
func testBalance() throws { func testBalance() throws {
@ -31,12 +31,12 @@ final class OfflineWalletTests: XCTestCase {
descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", descriptor: "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
network: Network.testnet network: Network.testnet
) )
let wallet: Wallet = try Wallet.newNoPersist( // let wallet: Wallet = try Wallet.newNoPersist(
descriptor: descriptor, // descriptor: descriptor,
changeDescriptor: nil, // changeDescriptor: nil,
network: .testnet // network: .testnet
) // )
//
XCTAssertEqual(wallet.getBalance().total, 0) // XCTAssertEqual(wallet.getBalance().total, 0)
} }
} }