use bdk::bitcoin::hashes::hex::ToHex; use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::util::bip32::DerivationPath as BdkDerivationPath; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, OutPoint as BdkOutPoint, Script, Txid}; use bdk::blockchain::any::{AnyBlockchain, AnyBlockchainConfig}; use bdk::blockchain::GetBlockHash; use bdk::blockchain::GetHeight; use bdk::blockchain::{ electrum::ElectrumBlockchainConfig, esplora::EsploraBlockchainConfig, ConfigurableBlockchain, }; use bdk::blockchain::{Blockchain as BdkBlockchain, Progress as BdkProgress}; use bdk::database::any::{AnyDatabase, SledDbConfiguration, SqliteDbConfiguration}; use bdk::database::{AnyDatabaseConfig, ConfigurableDatabase}; use bdk::descriptor::DescriptorXKey; use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::{ DerivableKey, DescriptorPublicKey as BdkDescriptorPublicKey, DescriptorSecretKey as BdkDescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey, }; use bdk::miniscript::BareCtx; use bdk::wallet::tx_builder::ChangeSpendPolicy; use bdk::wallet::AddressIndex as BdkAddressIndex; use bdk::wallet::AddressInfo as BdkAddressInfo; use bdk::{ BlockTime, Error, FeeRate, KeychainKind, SignOptions, SyncOptions as BdkSyncOptions, Wallet as BdkWallet, }; use std::collections::HashSet; use std::convert::{From, TryFrom}; use std::fmt; use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; uniffi_macros::include_scaffolding!("bdk"); type BdkError = Error; pub struct AddressAmount { pub address: String, pub amount: u64, } /// A derived address and the index it was found at. pub struct AddressInfo { /// Child index of this address pub index: u32, /// Address pub address: String, } impl From for AddressInfo { fn from(x: bdk::wallet::AddressInfo) -> AddressInfo { AddressInfo { index: x.index, address: x.address.to_string(), } } } /// The address index selection strategy to use to derived an address from the wallet's external /// descriptor. pub enum AddressIndex { /// Return a new address after incrementing the current descriptor index. New, /// Return the address for the current descriptor index if it has not been used in a received /// transaction. Otherwise return a new address as with AddressIndex::New. /// Use with caution, if the wallet has not yet detected an address has been used it could /// return an already used address. This function is primarily meant for situations where the /// caller is untrusted; for example when deriving donation addresses on-demand for a public /// web page. LastUnused, } impl From for BdkAddressIndex { fn from(x: AddressIndex) -> BdkAddressIndex { match x { AddressIndex::New => BdkAddressIndex::New, AddressIndex::LastUnused => BdkAddressIndex::LastUnused, } } } /// Type that can contain any of the database configurations defined by the library /// This allows storing a single configuration that can be loaded into an AnyDatabaseConfig /// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime /// will find this particularly useful. pub enum DatabaseConfig { /// Memory database has no config Memory, /// Simple key-value embedded database based on sled Sled { config: SledDbConfiguration }, /// Sqlite embedded database using rusqlite Sqlite { config: SqliteDbConfiguration }, } /// Configuration for an ElectrumBlockchain pub struct ElectrumConfig { /// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with ssl:// or tcp:// and include a port /// e.g. ssl://electrum.blockstream.info:60002 pub url: String, /// URL of the socks5 proxy server or a Tor service pub socks5: Option, /// Request retry count pub retry: u8, /// Request timeout (seconds) pub timeout: Option, /// Stop searching addresses for transactions after finding an unused gap of this length pub stop_gap: u64, } /// Configuration for an EsploraBlockchain pub struct EsploraConfig { /// Base URL of the esplora service /// e.g. https://blockstream.info/api/ pub base_url: String, /// Optional URL of the proxy to use to make requests to the Esplora server /// The string should be formatted as: ://:@host:. /// Note that the format of this value and the supported protocols change slightly between the /// sync version of esplora (using ureq) and the async version (using reqwest). For more /// details check with the documentation of the two crates. Both of them are compiled with /// the socks feature enabled. /// The proxy is ignored when targeting wasm32. pub proxy: Option, /// Number of parallel requests sent to the esplora service (default: 4) pub concurrency: Option, /// Stop searching addresses for transactions after finding an unused gap of this length. pub stop_gap: u64, /// Socket timeout. pub timeout: Option, } /// Type that can contain any of the blockchain configurations defined by the library. pub enum BlockchainConfig { /// Electrum client Electrum { config: ElectrumConfig }, /// Esplora client Esplora { config: EsploraConfig }, } /// A wallet transaction #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct TransactionDetails { /// Transaction id. pub txid: String, /// Received value (sats) /// Sum of owned outputs of this transaction. pub received: u64, /// Sent value (sats) /// Sum of owned inputs of this transaction. pub sent: u64, /// Fee value (sats) if available. /// The availability of the fee depends on the backend. It's never None with an Electrum /// Server backend, but it could be None with a Bitcoin RPC node without txindex that receive /// funds while offline. pub fee: Option, } /// A transaction, either of type Confirmed or Unconfirmed #[derive(Debug, Clone, PartialEq, Eq)] pub enum Transaction { /// A transaction that has yet to be included in a block Unconfirmed { /// The details of wallet transaction. details: TransactionDetails, }, /// A transaction that has been mined and is part of a block Confirmed { /// The details of wallet transaction details: TransactionDetails, /// Timestamp and block height of the block in which this transaction was mined confirmation: BlockTime, }, } impl From<&bdk::TransactionDetails> for TransactionDetails { fn from(x: &bdk::TransactionDetails) -> TransactionDetails { TransactionDetails { fee: x.fee, txid: x.txid.to_string(), received: x.received, sent: x.sent, } } } impl From<&bdk::TransactionDetails> for Transaction { fn from(x: &bdk::TransactionDetails) -> Transaction { match x.confirmation_time.clone() { Some(block_time) => Transaction::Confirmed { details: TransactionDetails::from(x), confirmation: block_time, }, None => Transaction::Unconfirmed { details: TransactionDetails::from(x), }, } } } struct Blockchain { blockchain_mutex: Mutex, } impl Blockchain { fn new(blockchain_config: BlockchainConfig) -> Result { let any_blockchain_config = match blockchain_config { BlockchainConfig::Electrum { config } => { AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { retry: config.retry, socks5: config.socks5, timeout: config.timeout, url: config.url, stop_gap: usize::try_from(config.stop_gap).unwrap(), }) } BlockchainConfig::Esplora { config } => { AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { base_url: config.base_url, proxy: config.proxy, concurrency: config.concurrency, stop_gap: usize::try_from(config.stop_gap).unwrap(), timeout: config.timeout, }) } }; let blockchain = AnyBlockchain::from_config(&any_blockchain_config)?; Ok(Self { blockchain_mutex: Mutex::new(blockchain), }) } fn get_blockchain(&self) -> MutexGuard { self.blockchain_mutex.lock().expect("blockchain") } fn broadcast(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<(), Error> { let tx = psbt.internal.lock().unwrap().clone().extract_tx(); self.get_blockchain().broadcast(&tx) } fn get_height(&self) -> Result { self.get_blockchain().get_height() } fn get_block_hash(&self, height: u32) -> Result { self.get_blockchain() .get_block_hash(u64::from(height)) .map(|hash| hash.to_string()) } } struct Wallet { wallet_mutex: Mutex>, } /// A reference to a transaction output. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct OutPoint { /// The referenced transaction's txid. txid: String, /// The index of the referenced output in its transaction's vout. vout: u32, } impl From<&OutPoint> for BdkOutPoint { fn from(x: &OutPoint) -> BdkOutPoint { BdkOutPoint { txid: Txid::from_str(&x.txid).unwrap(), vout: x.vout, } } } /// A transaction output, which defines new coins to be created from old ones. pub struct TxOut { /// The value of the output, in satoshis. value: u64, /// The address of the output. address: String, } pub struct LocalUtxo { outpoint: OutPoint, txout: TxOut, keychain: KeychainKind, is_spent: bool, } // This trait is used to convert the bdk TxOut type with field `script_pubkey: Script` // into the bdk-ffi TxOut type which has a field `address: String` instead trait NetworkLocalUtxo { fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo; } impl NetworkLocalUtxo for LocalUtxo { fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo { LocalUtxo { outpoint: OutPoint { txid: x.outpoint.txid.to_string(), vout: x.outpoint.vout, }, txout: TxOut { value: x.txout.value, address: bdk::bitcoin::util::address::Address::from_script( &x.txout.script_pubkey, network, ) .unwrap() .to_string(), }, keychain: x.keychain, is_spent: x.is_spent, } } } /// Trait that logs at level INFO every update received (if any). pub trait Progress: Send + Sync + 'static { /// Send a new progress update. The progress value should be in the range 0.0 - 100.0, and the message value is an /// optional text message that can be displayed to the user. fn update(&self, progress: f32, message: Option); } struct ProgressHolder { progress: Box, } impl BdkProgress for ProgressHolder { fn update(&self, progress: f32, message: Option) -> Result<(), Error> { self.progress.update(progress, message); Ok(()) } } impl fmt::Debug for ProgressHolder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ProgressHolder").finish_non_exhaustive() } } #[derive(Debug)] struct PartiallySignedBitcoinTransaction { internal: Mutex, } impl PartiallySignedBitcoinTransaction { fn new(psbt_base64: String) -> Result { let psbt: PartiallySignedTransaction = PartiallySignedTransaction::from_str(&psbt_base64)?; Ok(PartiallySignedBitcoinTransaction { internal: Mutex::new(psbt), }) } fn serialize(&self) -> String { let psbt = self.internal.lock().unwrap().clone(); psbt.to_string() } fn txid(&self) -> String { let tx = self.internal.lock().unwrap().clone().extract_tx(); let txid = tx.txid(); txid.to_hex() } } /// A Bitcoin wallet. /// The Wallet acts as a way of coherently interfacing with output descriptors and related transactions. Its main components are: /// 1. Output descriptors from which it can derive addresses. /// 2. A Database where it tracks transactions and utxos related to the descriptors. /// 3. Signers that can contribute signatures to addresses instantiated from the descriptors. impl Wallet { fn new( descriptor: String, change_descriptor: Option, network: Network, database_config: DatabaseConfig, ) -> Result { let any_database_config = match database_config { DatabaseConfig::Memory => AnyDatabaseConfig::Memory(()), DatabaseConfig::Sled { config } => AnyDatabaseConfig::Sled(config), DatabaseConfig::Sqlite { config } => AnyDatabaseConfig::Sqlite(config), }; let database = AnyDatabase::from_config(&any_database_config)?; let wallet_mutex = Mutex::new(BdkWallet::new( &descriptor, change_descriptor.as_ref(), network, database, )?); Ok(Wallet { wallet_mutex }) } fn get_wallet(&self) -> MutexGuard> { self.wallet_mutex.lock().expect("wallet") } /// Get the Bitcoin network the wallet is using. fn network(&self) -> Network { self.get_wallet().network() } /// Sync the internal database with the blockchain. fn sync( &self, blockchain: &Blockchain, progress: Option>, ) -> Result<(), BdkError> { let bdk_sync_opts = BdkSyncOptions { progress: progress.map(|p| { Box::new(ProgressHolder { progress: p }) as Box<(dyn bdk::blockchain::Progress + 'static)> }), }; let blockchain = blockchain.get_blockchain(); self.get_wallet().sync(blockchain.deref(), bdk_sync_opts) } /// Return a derived address using the external descriptor, see AddressIndex for available address index selection /// strategies. If none of the keys in the descriptor are derivable (i.e. the descriptor does not end with a * character) /// then the same address will always be returned for any AddressIndex. fn get_address(&self, address_index: AddressIndex) -> Result { self.get_wallet() .get_address(address_index.into()) .map(AddressInfo::from) } /// Return the balance, meaning the sum of this wallet’s unspent outputs’ values. Note that this method only operates /// on the internal database, which first needs to be Wallet.sync manually. fn get_balance(&self) -> Result { self.get_wallet().get_balance() } /// Sign a transaction with all the wallet’s signers. fn sign(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result { let mut psbt = psbt.internal.lock().unwrap(); self.get_wallet().sign(&mut psbt, SignOptions::default()) } /// Return the list of transactions made and received by the wallet. Note that this method only operate on the internal database, which first needs to be [Wallet.sync] manually. fn list_transactions(&self) -> Result, Error> { let transactions = self.get_wallet().list_transactions(true)?; Ok(transactions.iter().map(Transaction::from).collect()) } /// Return the list of unspent outputs of this wallet. Note that this method only operates on the internal database, /// which first needs to be Wallet.sync manually. fn list_unspent(&self) -> Result, Error> { let unspents = self.get_wallet().list_unspent()?; Ok(unspents .iter() .map(|u| LocalUtxo::from_utxo(u, self.network())) .collect()) } } fn to_script_pubkey(address: &str) -> Result { Address::from_str(address) .map(|x| x.script_pubkey()) .map_err(|e| BdkError::Generic(e.to_string())) } #[derive(Clone, Debug)] enum RbfValue { Default, Value(u32), } /// A transaction builder. /// After creating the TxBuilder, you set options on it until finally calling finish to consume the builder and generate the transaction. /// Each method on the TxBuilder returns an instance of a new TxBuilder with the option set/added. #[derive(Clone, Debug)] struct TxBuilder { recipients: Vec<(String, u64)>, utxos: Vec, unspendable: HashSet, change_policy: ChangeSpendPolicy, manually_selected_only: bool, fee_rate: Option, fee_absolute: Option, drain_wallet: bool, drain_to: Option, rbf: Option, data: Vec, } impl TxBuilder { fn new() -> Self { TxBuilder { recipients: Vec::new(), utxos: Vec::new(), unspendable: HashSet::new(), change_policy: ChangeSpendPolicy::ChangeAllowed, manually_selected_only: false, fee_rate: None, fee_absolute: None, drain_wallet: false, drain_to: None, rbf: None, data: Vec::new(), } } /// Add a recipient to the internal list. fn add_recipient(&self, recipient: String, amount: u64) -> Arc { let mut recipients = self.recipients.to_vec(); recipients.append(&mut vec![(recipient, amount)]); Arc::new(TxBuilder { recipients, ..self.clone() }) } fn set_recipients(&self, recipients: Vec) -> Arc { let recipients = recipients .iter() .map(|address_amount| (address_amount.address.clone(), address_amount.amount)) .collect(); Arc::new(TxBuilder { recipients, ..self.clone() }) } /// Add a utxo to the internal list of unspendable utxos. It’s important to note that the "must-be-spent" /// utxos added with [TxBuilder.addUtxo] have priority over this. See the Rust docs of the two linked methods for more details. fn add_unspendable(&self, unspendable: OutPoint) -> Arc { let mut unspendable_hash_set = self.unspendable.clone(); unspendable_hash_set.insert(unspendable); Arc::new(TxBuilder { unspendable: unspendable_hash_set, ..self.clone() }) } /// Add an outpoint to the internal list of UTXOs that must be spent. These have priority over the "unspendable" /// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent. fn add_utxo(&self, outpoint: OutPoint) -> Arc { self.add_utxos(vec![outpoint]) } /// Add the list of outpoints to the internal list of UTXOs that must be spent. If an error occurs while adding /// any of the UTXOs then none of them are added and the error is returned. These have priority over the "unspendable" /// utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent. fn add_utxos(&self, mut outpoints: Vec) -> Arc { let mut utxos = self.utxos.to_vec(); utxos.append(&mut outpoints); Arc::new(TxBuilder { utxos, ..self.clone() }) } /// Do not spend change outputs. This effectively adds all the change outputs to the "unspendable" list. See TxBuilder.unspendable. fn do_not_spend_change(&self) -> Arc { Arc::new(TxBuilder { change_policy: ChangeSpendPolicy::ChangeForbidden, ..self.clone() }) } /// Only spend utxos added by [add_utxo]. The wallet will not add additional utxos to the transaction even if they are /// needed to make the transaction valid. fn manually_selected_only(&self) -> Arc { Arc::new(TxBuilder { manually_selected_only: true, ..self.clone() }) } /// Only spend change outputs. This effectively adds all the non-change outputs to the "unspendable" list. See TxBuilder.unspendable. fn only_spend_change(&self) -> Arc { Arc::new(TxBuilder { change_policy: ChangeSpendPolicy::OnlyChange, ..self.clone() }) } /// Replace the internal list of unspendable utxos with a new list. It’s important to note that the "must-be-spent" utxos added with /// TxBuilder.addUtxo have priority over these. See the Rust docs of the two linked methods for more details. fn unspendable(&self, unspendable: Vec) -> Arc { Arc::new(TxBuilder { unspendable: unspendable.into_iter().collect(), ..self.clone() }) } /// Set a custom fee rate. fn fee_rate(&self, sat_per_vb: f32) -> Arc { Arc::new(TxBuilder { fee_rate: Some(sat_per_vb), ..self.clone() }) } /// Set an absolute fee. fn fee_absolute(&self, fee_amount: u64) -> Arc { Arc::new(TxBuilder { fee_absolute: Some(fee_amount), ..self.clone() }) } /// Spend all the available inputs. This respects filters like TxBuilder.unspendable and the change policy. fn drain_wallet(&self) -> Arc { Arc::new(TxBuilder { drain_wallet: true, ..self.clone() }) } /// Sets the address to drain excess coins to. Usually, when there are excess coins they are sent to a change address /// generated by the wallet. This option replaces the usual change address with an arbitrary ScriptPubKey of your choosing. /// Just as with a change output, if the drain output is not needed (the excess coins are too small) it will not be included /// in the resulting transaction. The only difference is that it is valid to use drain_to without setting any ordinary recipients /// with add_recipient (but it is perfectly fine to add recipients as well). If you choose not to set any recipients, you should /// either provide the utxos that the transaction should spend via add_utxos, or set drain_wallet to spend all of them. /// When bumping the fees of a transaction made with this option, you probably want to use BumpFeeTxBuilder.allow_shrinking /// to allow this output to be reduced to pay for the extra fees. fn drain_to(&self, address: String) -> Arc { Arc::new(TxBuilder { drain_to: Some(address), ..self.clone() }) } /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. fn enable_rbf(&self) -> Arc { Arc::new(TxBuilder { rbf: Some(RbfValue::Default), ..self.clone() }) } /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD` /// an error will be thrown, since it would not be a valid nSequence to signal RBF. fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc { Arc::new(TxBuilder { rbf: Some(RbfValue::Value(nsequence)), ..self.clone() }) } /// Add data as an output using OP_RETURN. fn add_data(&self, data: Vec) -> Arc { Arc::new(TxBuilder { data, ..self.clone() }) } /// Finish building the transaction. Returns the BIP174 PSBT. fn finish(&self, wallet: &Wallet) -> Result, Error> { let wallet = wallet.get_wallet(); let mut tx_builder = wallet.build_tx(); for (address, amount) in &self.recipients { tx_builder.add_recipient(to_script_pubkey(address)?, *amount); } tx_builder.change_policy(self.change_policy); if !self.utxos.is_empty() { let bdk_utxos: Vec = self.utxos.iter().map(BdkOutPoint::from).collect(); let utxos: &[BdkOutPoint] = &bdk_utxos; tx_builder.add_utxos(utxos)?; } if !self.unspendable.is_empty() { let bdk_unspendable: Vec = self.unspendable.iter().map(BdkOutPoint::from).collect(); tx_builder.unspendable(bdk_unspendable); } if self.manually_selected_only { tx_builder.manually_selected_only(); } if let Some(sat_per_vb) = self.fee_rate { tx_builder.fee_rate(FeeRate::from_sat_per_vb(sat_per_vb)); } if let Some(fee_amount) = self.fee_absolute { tx_builder.fee_absolute(fee_amount); } if self.drain_wallet { tx_builder.drain_wallet(); } if let Some(address) = &self.drain_to { tx_builder.drain_to(to_script_pubkey(address)?); } if let Some(rbf) = &self.rbf { match *rbf { RbfValue::Default => { tx_builder.enable_rbf(); } RbfValue::Value(nsequence) => { tx_builder.enable_rbf_with_sequence(nsequence); } } } if !&self.data.is_empty() { tx_builder.add_data(self.data.as_slice()); } tx_builder .finish() .map(|(psbt, _)| PartiallySignedBitcoinTransaction { internal: Mutex::new(psbt), }) .map(Arc::new) } } /// The BumpFeeTxBuilder is used to bump the fee on a transaction that has been broadcast and has its RBF flag set to true. #[derive(Clone)] struct BumpFeeTxBuilder { txid: String, fee_rate: f32, allow_shrinking: Option, rbf: Option, } impl BumpFeeTxBuilder { fn new(txid: String, fee_rate: f32) -> Self { Self { txid, fee_rate, allow_shrinking: None, rbf: None, } } /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this script_pubkey /// in order to bump the transaction fee. Without specifying this the wallet will attempt to find a change output to /// shrink instead. Note that the output may shrink to below the dust limit and therefore be removed. If it is preserved /// then it is currently not guaranteed to be in the same position as it was originally. Returns an error if script_pubkey /// can’t be found among the recipients of the transaction we are bumping. fn allow_shrinking(&self, address: String) -> Arc { Arc::new(Self { allow_shrinking: Some(address), ..self.clone() }) } /// Enable signaling RBF. This will use the default `nsequence` value of `0xFFFFFFFD`. fn enable_rbf(&self) -> Arc { Arc::new(Self { rbf: Some(RbfValue::Default), ..self.clone() }) } /// Enable signaling RBF with a specific nSequence value. This can cause conflicts if the wallet's descriptors contain an /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. If the `nsequence` is higher than `0xFFFFFFFD` /// an error will be thrown, since it would not be a valid nSequence to signal RBF. fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc { Arc::new(Self { rbf: Some(RbfValue::Value(nsequence)), ..self.clone() }) } /// Finish building the transaction. Returns the BIP174 PSBT. fn finish(&self, wallet: &Wallet) -> Result, Error> { let wallet = wallet.get_wallet(); let txid = Txid::from_str(self.txid.as_str())?; let mut tx_builder = wallet.build_fee_bump(txid)?; tx_builder.fee_rate(FeeRate::from_sat_per_vb(self.fee_rate)); if let Some(allow_shrinking) = &self.allow_shrinking { let address = Address::from_str(allow_shrinking).map_err(|e| Error::Generic(e.to_string()))?; let script = address.script_pubkey(); tx_builder.allow_shrinking(script)?; } if let Some(rbf) = &self.rbf { match *rbf { RbfValue::Default => { tx_builder.enable_rbf(); } RbfValue::Value(nsequence) => { tx_builder.enable_rbf_with_sequence(nsequence); } } } tx_builder .finish() .map(|(psbt, _)| PartiallySignedBitcoinTransaction { internal: Mutex::new(psbt), }) .map(Arc::new) } } fn generate_mnemonic(word_count: WordCount) -> Result { let mnemonic: GeneratedKey<_, BareCtx> = Mnemonic::generate((word_count, Language::English)).unwrap(); Ok(mnemonic.to_string()) } struct DerivationPath { derivation_path_mutex: Mutex, } impl DerivationPath { fn new(path: String) -> Result { BdkDerivationPath::from_str(&path) .map(|x| DerivationPath { derivation_path_mutex: Mutex::new(x), }) .map_err(|e| BdkError::Generic(e.to_string())) } } struct DescriptorSecretKey { descriptor_secret_key_mutex: Mutex, } impl DescriptorSecretKey { fn new(network: Network, mnemonic: String, password: Option) -> Result { let mnemonic = Mnemonic::parse_in(Language::English, mnemonic) .map_err(|e| BdkError::Generic(e.to_string()))?; let xkey: ExtendedKey = (mnemonic, password).into_extended_key()?; let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { origin: None, xkey: xkey.into_xprv(network).unwrap(), derivation_path: BdkDerivationPath::master(), wildcard: bdk::descriptor::Wildcard::Unhardened, }); Ok(Self { descriptor_secret_key_mutex: Mutex::new(descriptor_secret_key), }) } fn derive(&self, path: Arc) -> Result, BdkError> { let secp = Secp256k1::new(); let descriptor_secret_key = self.descriptor_secret_key_mutex.lock().unwrap(); let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); match descriptor_secret_key.deref() { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let derived_xprv = descriptor_x_key.xkey.derive_priv(&secp, &path)?; let key_source = match descriptor_x_key.origin.clone() { Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), None => (descriptor_x_key.xkey.fingerprint(&secp), path), }; let derived_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { origin: Some(key_source), xkey: derived_xprv, derivation_path: BdkDerivationPath::default(), wildcard: descriptor_x_key.wildcard, }); Ok(Arc::new(Self { descriptor_secret_key_mutex: Mutex::new(derived_descriptor_secret_key), })) } BdkDescriptorSecretKey::SinglePriv(_) => { unreachable!() } } } fn extend(&self, path: Arc) -> Arc { let descriptor_secret_key = self.descriptor_secret_key_mutex.lock().unwrap(); let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); match descriptor_secret_key.deref() { BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let extended_path = descriptor_x_key.derivation_path.extend(path); let extended_descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { origin: descriptor_x_key.origin.clone(), xkey: descriptor_x_key.xkey, derivation_path: extended_path, wildcard: descriptor_x_key.wildcard, }); Arc::new(Self { descriptor_secret_key_mutex: Mutex::new(extended_descriptor_secret_key), }) } BdkDescriptorSecretKey::SinglePriv(_) => { unreachable!() } } } fn as_public(&self) -> Arc { let secp = Secp256k1::new(); let descriptor_public_key = self .descriptor_secret_key_mutex .lock() .unwrap() .as_public(&secp) .unwrap(); Arc::new(DescriptorPublicKey { descriptor_public_key_mutex: Mutex::new(descriptor_public_key), }) } fn as_string(&self) -> String { self.descriptor_secret_key_mutex.lock().unwrap().to_string() } } struct DescriptorPublicKey { descriptor_public_key_mutex: Mutex, } impl DescriptorPublicKey { fn derive(&self, path: Arc) -> Result, BdkError> { let secp = Secp256k1::new(); let descriptor_public_key = self.descriptor_public_key_mutex.lock().unwrap(); let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); match descriptor_public_key.deref() { BdkDescriptorPublicKey::XPub(descriptor_x_key) => { let derived_xpub = descriptor_x_key.xkey.derive_pub(&secp, &path)?; let key_source = match descriptor_x_key.origin.clone() { Some((fingerprint, origin_path)) => (fingerprint, origin_path.extend(path)), None => (descriptor_x_key.xkey.fingerprint(), path), }; let derived_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey { origin: Some(key_source), xkey: derived_xpub, derivation_path: BdkDerivationPath::default(), wildcard: descriptor_x_key.wildcard, }); Ok(Arc::new(Self { descriptor_public_key_mutex: Mutex::new(derived_descriptor_public_key), })) } BdkDescriptorPublicKey::SinglePub(_) => { unreachable!() } } } fn extend(&self, path: Arc) -> Arc { let descriptor_public_key = self.descriptor_public_key_mutex.lock().unwrap(); let path = path.derivation_path_mutex.lock().unwrap().deref().clone(); match descriptor_public_key.deref() { BdkDescriptorPublicKey::XPub(descriptor_x_key) => { let extended_path = descriptor_x_key.derivation_path.extend(path); let extended_descriptor_public_key = BdkDescriptorPublicKey::XPub(DescriptorXKey { origin: descriptor_x_key.origin.clone(), xkey: descriptor_x_key.xkey, derivation_path: extended_path, wildcard: descriptor_x_key.wildcard, }); Arc::new(Self { descriptor_public_key_mutex: Mutex::new(extended_descriptor_public_key), }) } BdkDescriptorPublicKey::SinglePub(_) => { unreachable!() } } } fn as_string(&self) -> String { self.descriptor_public_key_mutex.lock().unwrap().to_string() } } uniffi::deps::static_assertions::assert_impl_all!(Wallet: Sync, Send); // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs. // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk` // crate. #[cfg(test)] mod test { use crate::*; use bdk::bitcoin::Address; use bdk::bitcoin::Network::Testnet; use bdk::wallet::get_funded_wallet; use std::str::FromStr; use std::sync::Mutex; #[test] fn test_drain_wallet() { let test_wpkh = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let (funded_wallet, _, _) = get_funded_wallet(test_wpkh); let test_wallet = Wallet { wallet_mutex: Mutex::new(funded_wallet), }; let drain_to_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt".to_string(); let tx_builder = TxBuilder::new() .drain_wallet() .drain_to(drain_to_address.clone()); //dbg!(&tx_builder); assert!(tx_builder.drain_wallet); assert_eq!(tx_builder.drain_to, Some(drain_to_address)); let psbt = tx_builder.finish(&test_wallet).unwrap(); let psbt = psbt.internal.lock().unwrap().clone(); // confirm one input with 50,000 sats assert_eq!(psbt.inputs.len(), 1); let input_value = psbt .inputs .get(0) .cloned() .unwrap() .non_witness_utxo .unwrap() .output .get(0) .unwrap() .value; assert_eq!(input_value, 50_000_u64); // confirm one output to correct address with all sats - fee assert_eq!(psbt.outputs.len(), 1); let output_address = Address::from_script( &psbt .unsigned_tx .output .get(0) .cloned() .unwrap() .script_pubkey, Testnet, ) .unwrap(); assert_eq!( output_address, Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap() ); let output_value = psbt.unsigned_tx.output.get(0).cloned().unwrap().value; assert_eq!(output_value, 49_890_u64); // input - fee } fn get_descriptor_secret_key() -> DescriptorSecretKey { let mnemonic = "chaos fabric time speed sponsor all flat solution wisdom trophy crack object robot pave observe combine where aware bench orient secret primary cable detect".to_string(); DescriptorSecretKey::new(Network::Testnet, mnemonic, None).unwrap() } fn derive_dsk( key: &DescriptorSecretKey, path: &str, ) -> Result, BdkError> { let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); key.derive(path) } fn extend_dsk(key: &DescriptorSecretKey, path: &str) -> Arc { let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); key.extend(path) } fn derive_dpk( key: &DescriptorPublicKey, path: &str, ) -> Result, BdkError> { let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); key.derive(path) } fn extend_dpk(key: &DescriptorPublicKey, path: &str) -> Arc { let path = Arc::new(DerivationPath::new(path.to_string()).unwrap()); key.extend(path) } #[test] fn test_generate_descriptor_secret_key() { let master_dsk = get_descriptor_secret_key(); assert_eq!(master_dsk.as_string(), "tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/*"); assert_eq!(master_dsk.as_public().as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/*"); } #[test] fn test_derive_self() { let master_dsk = get_descriptor_secret_key(); let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m").unwrap(); assert_eq!(derived_dsk.as_string(), "[d1d04177]tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/*"); let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); let derived_dpk: &DescriptorPublicKey = &derive_dpk(master_dpk, "m").unwrap(); assert_eq!(derived_dpk.as_string(), "[d1d04177]tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/*"); } #[test] fn test_derive_descriptors_keys() { let master_dsk = get_descriptor_secret_key(); let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m/0").unwrap(); assert_eq!(derived_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/*"); let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); let derived_dpk: &DescriptorPublicKey = &derive_dpk(master_dpk, "m/0").unwrap(); assert_eq!(derived_dpk.as_string(), "[d1d04177/0]tpubD9oaCiP1MPmQdndm7DCD3D3QU34pWd6BbKSRedoZF1UJcNhEk3PJwkALNYkhxeTKL29oGNR7psqvT1KZydCGqUDEKXN6dVQJY2R8ooLPy8m/*"); } #[test] fn test_extend_descriptor_keys() { let master_dsk = get_descriptor_secret_key(); let extended_dsk: &DescriptorSecretKey = &extend_dsk(&master_dsk, "m/0"); assert_eq!(extended_dsk.as_string(), "tprv8ZgxMBicQKsPdWuqM1t1CDRvQtQuBPyfL6GbhQwtxDKgUAVPbxmj71pRA8raTqLrec5LyTs5TqCxdABcZr77bt2KyWA5bizJHnC4g4ysm4h/0/*"); let master_dpk: &DescriptorPublicKey = &master_dsk.as_public(); let extended_dpk: &DescriptorPublicKey = &extend_dpk(master_dpk, "m/0"); assert_eq!(extended_dpk.as_string(), "tpubD6NzVbkrYhZ4WywdEfYbbd62yuvqLjAZuPsNyvzCNV85JekAEMbKHWSHLF9h3j45SxewXDcLv328B1SEZrxg4iwGfmdt1pDFjZiTkGiFqGa/0/*"); } #[test] fn test_derive_and_extend_descriptor_secret_key() { let master_dsk = get_descriptor_secret_key(); // derive DescriptorSecretKey with path "m/0" from master let derived_dsk: &DescriptorSecretKey = &derive_dsk(&master_dsk, "m/0").unwrap(); assert_eq!(derived_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/*"); // extend derived_dsk with path "m/0" let extended_dsk: &DescriptorSecretKey = &extend_dsk(derived_dsk, "m/0"); assert_eq!(extended_dsk.as_string(), "[d1d04177/0]tprv8d7Y4JLmD25jkKbyDZXcdoPHu1YtMHuH21qeN7mFpjfumtSU7eZimFYUCSa3MYzkEYfSNRBV34GEr2QXwZCMYRZ7M1g6PUtiLhbJhBZEGYJ/0/*"); } #[test] fn test_derive_hardened_path_using_public() { let master_dpk = get_descriptor_secret_key().as_public(); let derived_dpk = &derive_dpk(&master_dpk, "m/84h/1h/0h"); assert!(derived_dpk.is_err()); } }