diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index 7bdfee0a..4f0b8603 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -459,6 +459,9 @@ interface Wallet { /// Internally calls [`Self::public_descriptor`] to fetch the right descriptor string descriptor_checksum(KeychainKind keychain); + [Throws=DescriptorError] + Policy? policies(KeychainKind keychain); + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. Balance balance(); @@ -569,6 +572,60 @@ interface Wallet { interface Update {}; +interface Policy { + string id(); + + string as_string(); + + boolean requires_path(); + + SatisfiableItem item(); + + Satisfaction satisfaction(); + + Satisfaction contribution(); +}; + +[Enum] +interface SatisfiableItem { + EcdsaSignature(PkOrF key); + SchnorrSignature(PkOrF key); + Sha256Preimage(string hash); + Hash256Preimage(string hash); + Ripemd160Preimage(string hash); + Hash160Preimage(string hash); + AbsoluteTimelock(LockTime value); + RelativeTimelock(u32 value); + Multisig(sequence keys, u64 threshold); + Thresh(sequence items, u64 threshold); +}; + +[Enum] +interface PkOrF { + Pubkey(string value); + XOnlyPubkey(string value); + Fingerprint(string value); +}; + +[Enum] +interface LockTime { + Blocks(u32 height); + Seconds(u32 consensus_time); +}; + +[Enum] +interface Satisfaction { + Partial(u64 n, u64 m, sequence items, boolean? sorted, record> conditions); + PartialComplete(u64 n, u64 m, sequence items, boolean? sorted, record, sequence> conditions); + Complete(Condition condition); + None(string msg); +}; + +dictionary Condition { + u32? csv; + LockTime? timelock; +}; + interface TxBuilder { constructor(); @@ -584,6 +641,8 @@ interface TxBuilder { TxBuilder add_utxo(OutPoint outpoint); + TxBuilder policy_path(record> policy_path, KeychainKind keychain); + TxBuilder change_policy(ChangeSpendPolicy change_policy); TxBuilder do_not_spend_change(); diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index b8db0d78..2a082439 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -51,12 +51,18 @@ use crate::types::Balance; use crate::types::BlockId; use crate::types::CanonicalTx; use crate::types::ChainPosition; +use crate::types::Condition; use crate::types::ConfirmationBlockTime; use crate::types::FullScanRequest; use crate::types::FullScanRequestBuilder; use crate::types::FullScanScriptInspector; use crate::types::KeychainAndIndex; use crate::types::LocalOutput; +use crate::types::LockTime; +use crate::types::PkOrF; +use crate::types::Policy; +use crate::types::Satisfaction; +use crate::types::SatisfiableItem; use crate::types::ScriptAmount; use crate::types::SentAndReceivedValues; use crate::types::SyncRequest; diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 7ade7c30..c11e6b0b 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -3,6 +3,7 @@ use crate::error::CreateTxError; use crate::types::ScriptAmount; use crate::wallet::Wallet; +use bdk_wallet::KeychainKind; use bitcoin_ffi::{Amount, FeeRate, Script}; use bdk_wallet::bitcoin::amount::Amount as BdkAmount; @@ -11,6 +12,8 @@ use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf; use bdk_wallet::bitcoin::{OutPoint, Sequence, Txid}; use bdk_wallet::ChangeSpendPolicy; +use std::collections::BTreeMap; +use std::collections::HashMap; use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; @@ -21,6 +24,8 @@ pub struct TxBuilder { pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>, pub(crate) utxos: Vec, pub(crate) unspendable: HashSet, + pub(crate) internal_policy_path: Option>>, + pub(crate) external_policy_path: Option>>, pub(crate) change_policy: ChangeSpendPolicy, pub(crate) manually_selected_only: bool, pub(crate) fee_rate: Option, @@ -37,6 +42,8 @@ impl TxBuilder { recipients: Vec::new(), utxos: Vec::new(), unspendable: HashSet::new(), + internal_policy_path: None, + external_policy_path: None, change_policy: ChangeSpendPolicy::ChangeAllowed, manually_selected_only: false, fee_rate: None, @@ -104,6 +111,25 @@ impl TxBuilder { }) } + pub(crate) fn policy_path( + &self, + policy_path: HashMap>, + keychain: KeychainKind, + ) -> Arc { + let mut updated_self = self.clone(); + let to_update = match keychain { + KeychainKind::Internal => &mut updated_self.internal_policy_path, + KeychainKind::External => &mut updated_self.external_policy_path, + }; + *to_update = Some( + policy_path + .into_iter() + .map(|(key, value)| (key, value.into_iter().map(|x| x as usize).collect())) + .collect::>>(), + ); + Arc::new(updated_self) + } + pub(crate) fn change_policy(&self, change_policy: ChangeSpendPolicy) -> Arc { Arc::new(TxBuilder { change_policy, @@ -177,6 +203,12 @@ impl TxBuilder { for (script, amount) in &self.recipients { tx_builder.add_recipient(script.clone(), *amount); } + if let Some(policy_path) = &self.external_policy_path { + tx_builder.policy_path(policy_path.clone(), KeychainKind::External); + } + if let Some(policy_path) = &self.internal_policy_path { + tx_builder.policy_path(policy_path.clone(), KeychainKind::Internal); + } tx_builder.change_policy(self.change_policy); if !self.utxos.is_empty() { tx_builder @@ -251,3 +283,92 @@ impl BumpFeeTxBuilder { Ok(Arc::new(psbt.into())) } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use bitcoin_ffi::Network; + + use crate::{ + descriptor::Descriptor, esplora::EsploraClient, store::Connection, + types::FullScanScriptInspector, wallet::Wallet, + }; + + struct FullScanInspector; + impl FullScanScriptInspector for FullScanInspector { + fn inspect(&self, _: bdk_wallet::KeychainKind, _: u32, _: Arc) {} + } + + #[test] + fn test_policy_path() { + let wallet = create_and_sync_wallet(); + let address = wallet + .next_unused_address(bdk_wallet::KeychainKind::External) + .address; + println!("Wallet address: {:?}", address); + + let ext_policy = wallet.policies(bdk_wallet::KeychainKind::External); + let int_policy = wallet.policies(bdk_wallet::KeychainKind::Internal); + + if let (Ok(Some(ext_policy)), Ok(Some(int_policy))) = (ext_policy, int_policy) { + let ext_path = vec![(ext_policy.id().clone(), vec![0, 1])] + .into_iter() + .collect(); + println!("External Policy path : {:?}\n", ext_path); + let int_path = vec![(int_policy.id().clone(), vec![0, 1])] + .into_iter() + .collect(); + println!("Internal Policy Path: {:?}\n", int_path); + + match crate::tx_builder::TxBuilder::new() + .add_recipient( + &(*address.script_pubkey()).to_owned(), + Arc::new(bitcoin_ffi::Amount::from_sat(1000)), + ) + .do_not_spend_change() + .policy_path(int_path, bdk_wallet::KeychainKind::Internal) + .policy_path(ext_path, bdk_wallet::KeychainKind::External) + .finish(&Arc::new(wallet)) + { + Ok(tx) => println!("Transaction serialized: {}\n", tx.serialize()), + Err(e) => eprintln!("Error: {:?}", e), + } + } else { + println!("Failed to retrieve valid policies for keychains."); + } + } + + fn create_and_sync_wallet() -> Wallet { + let external_descriptor = format!( + "wsh(thresh(2,pk({}/0/*),sj:and_v(v:pk({}/0/*),n:older(6)),snj:and_v(v:pk({}/0/*),after(630000))))", + "tpubD6NzVbkrYhZ4XJBfEJ6gt9DiVdfWJijsQTCE3jtXByW3Tk6AVGQ3vL1NNxg3SjB7QkJAuutACCQjrXD8zdZSM1ZmBENszCqy49ECEHmD6rf", + "tpubD6NzVbkrYhZ4YfAr3jCBRk4SpqB9L1Hh442y83njwfMaker7EqZd7fHMqyTWrfRYJ1e5t2ue6BYjW5i5yQnmwqbzY1a3kfqNxog1AFcD1aE", + "tprv8ZgxMBicQKsPeitVUz3s6cfyCECovNP7t82FaKPa4UKqV1kssWcXgLkMDjzDbgG9GWoza4pL7z727QitfzkiwX99E1Has3T3a1MKHvYWmQZ" + ); + let internal_descriptor = format!( + "wsh(thresh(2,pk({}/1/*),sj:and_v(v:pk({}/1/*),n:older(6)),snj:and_v(v:pk({}/1/*),after(630000))))", + "tpubD6NzVbkrYhZ4XJBfEJ6gt9DiVdfWJijsQTCE3jtXByW3Tk6AVGQ3vL1NNxg3SjB7QkJAuutACCQjrXD8zdZSM1ZmBENszCqy49ECEHmD6rf", + "tpubD6NzVbkrYhZ4YfAr3jCBRk4SpqB9L1Hh442y83njwfMaker7EqZd7fHMqyTWrfRYJ1e5t2ue6BYjW5i5yQnmwqbzY1a3kfqNxog1AFcD1aE", + "tprv8ZgxMBicQKsPeitVUz3s6cfyCECovNP7t82FaKPa4UKqV1kssWcXgLkMDjzDbgG9GWoza4pL7z727QitfzkiwX99E1Has3T3a1MKHvYWmQZ" + ); + let wallet = Wallet::new( + Arc::new(Descriptor::new(external_descriptor, Network::Signet).unwrap()), + Arc::new(Descriptor::new(internal_descriptor, Network::Signet).unwrap()), + Network::Signet, + Arc::new(Connection::new_in_memory().unwrap()), + ) + .unwrap(); + let client = EsploraClient::new("https://mutinynet.com/api/".to_string()); + let full_scan_builder = wallet.start_full_scan(); + let full_scan_request = full_scan_builder + .inspect_spks_for_all_keychains(Arc::new(FullScanInspector)) + .unwrap() + .build() + .unwrap(); + let update = client.full_scan(full_scan_request, 10, 10).unwrap(); + wallet.apply_update(update).unwrap(); + println!("Wallet balance: {:?}", wallet.balance().total.to_sat()); + wallet + } +} diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index eaf3a2ac..91bea8ed 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -5,7 +5,9 @@ use bitcoin_ffi::Amount; use bitcoin_ffi::OutPoint; use bitcoin_ffi::Script; +use bdk_core::bitcoin::absolute::LockTime as BdkLockTime; use bdk_core::spk_client::SyncItem; + use bdk_wallet::bitcoin::Transaction as BdkTransaction; use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest; use bdk_wallet::chain::spk_client::FullScanRequestBuilder as BdkFullScanRequestBuilder; @@ -15,12 +17,18 @@ use bdk_wallet::chain::tx_graph::CanonicalTx as BdkCanonicalTx; use bdk_wallet::chain::{ ChainPosition as BdkChainPosition, ConfirmationBlockTime as BdkConfirmationBlockTime, }; + +use bdk_wallet::descriptor::policy::{ + Condition as BdkCondition, PkOrF as BdkPkOrF, Policy as BdkPolicy, + Satisfaction as BdkSatisfaction, SatisfiableItem as BdkSatisfiableItem, +}; use bdk_wallet::AddressInfo as BdkAddressInfo; use bdk_wallet::Balance as BdkBalance; use bdk_wallet::KeychainKind; use bdk_wallet::LocalOutput as BdkLocalOutput; use bdk_wallet::Update as BdkUpdate; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; #[derive(Debug)] @@ -237,3 +245,256 @@ pub struct KeychainAndIndex { pub keychain: KeychainKind, pub index: u32, } + +/// Descriptor spending policy +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Policy(BdkPolicy); +impl From for Policy { + fn from(value: BdkPolicy) -> Self { + Policy(value) + } +} +impl From for BdkPolicy { + fn from(value: Policy) -> Self { + value.0 + } +} +impl Policy { + pub fn id(&self) -> String { + self.0.id.clone() + } + + pub fn as_string(&self) -> String { + bdk_wallet::serde_json::to_string(&self.0).unwrap() + } + + pub fn requires_path(&self) -> bool { + self.0.requires_path() + } + + pub fn item(&self) -> SatisfiableItem { + self.0.item.clone().into() + } + + pub fn satisfaction(&self) -> Satisfaction { + self.0.satisfaction.clone().into() + } + + pub fn contribution(&self) -> Satisfaction { + self.0.contribution.clone().into() + } +} + +#[derive(Debug, Clone)] +pub enum SatisfiableItem { + EcdsaSignature { + key: PkOrF, + }, + SchnorrSignature { + key: PkOrF, + }, + Sha256Preimage { + hash: String, + }, + Hash256Preimage { + hash: String, + }, + Ripemd160Preimage { + hash: String, + }, + Hash160Preimage { + hash: String, + }, + AbsoluteTimelock { + value: LockTime, + }, + RelativeTimelock { + value: u32, + }, + Multisig { + keys: Vec, + threshold: u64, + }, + Thresh { + items: Vec>, + threshold: u64, + }, +} + +impl From for SatisfiableItem { + fn from(value: BdkSatisfiableItem) -> Self { + match value { + BdkSatisfiableItem::EcdsaSignature(pk_or_f) => SatisfiableItem::EcdsaSignature { + key: pk_or_f.into(), + }, + BdkSatisfiableItem::SchnorrSignature(pk_or_f) => SatisfiableItem::SchnorrSignature { + key: pk_or_f.into(), + }, + BdkSatisfiableItem::Sha256Preimage { hash } => SatisfiableItem::Sha256Preimage { + hash: hash.to_string(), + }, + BdkSatisfiableItem::Hash256Preimage { hash } => SatisfiableItem::Hash256Preimage { + hash: hash.to_string(), + }, + BdkSatisfiableItem::Ripemd160Preimage { hash } => SatisfiableItem::Ripemd160Preimage { + hash: hash.to_string(), + }, + BdkSatisfiableItem::Hash160Preimage { hash } => SatisfiableItem::Hash160Preimage { + hash: hash.to_string(), + }, + BdkSatisfiableItem::AbsoluteTimelock { value } => SatisfiableItem::AbsoluteTimelock { + value: value.into(), + }, + BdkSatisfiableItem::RelativeTimelock { value } => SatisfiableItem::RelativeTimelock { + value: value.to_consensus_u32(), + }, + BdkSatisfiableItem::Multisig { keys, threshold } => SatisfiableItem::Multisig { + keys: keys.iter().map(|e| e.to_owned().into()).collect(), + threshold: threshold as u64, + }, + BdkSatisfiableItem::Thresh { items, threshold } => SatisfiableItem::Thresh { + items: items + .iter() + .map(|e| Arc::new(e.to_owned().into())) + .collect(), + threshold: threshold as u64, + }, + } + } +} + +#[derive(Debug, Clone)] +pub enum PkOrF { + Pubkey { value: String }, + XOnlyPubkey { value: String }, + Fingerprint { value: String }, +} + +impl From for PkOrF { + fn from(value: BdkPkOrF) -> Self { + match value { + BdkPkOrF::Pubkey(public_key) => PkOrF::Pubkey { + value: public_key.to_string(), + }, + BdkPkOrF::XOnlyPubkey(xonly_public_key) => PkOrF::XOnlyPubkey { + value: xonly_public_key.to_string(), + }, + BdkPkOrF::Fingerprint(fingerprint) => PkOrF::Fingerprint { + value: fingerprint.to_string(), + }, + } + } +} + +#[derive(Debug, Clone)] +pub enum LockTime { + Blocks { height: u32 }, + Seconds { consensus_time: u32 }, +} + +impl From for LockTime { + fn from(value: BdkLockTime) -> Self { + match value { + BdkLockTime::Blocks(height) => LockTime::Blocks { + height: height.to_consensus_u32(), + }, + BdkLockTime::Seconds(time) => LockTime::Seconds { + consensus_time: time.to_consensus_u32(), + }, + } + } +} + +#[derive(Debug, Clone)] +pub enum Satisfaction { + Partial { + n: u64, + m: u64, + items: Vec, + sorted: Option, + conditions: HashMap>, + }, + PartialComplete { + n: u64, + m: u64, + items: Vec, + sorted: Option, + conditions: HashMap, Vec>, + }, + Complete { + condition: Condition, + }, + None { + msg: String, + }, +} + +impl From for Satisfaction { + fn from(value: BdkSatisfaction) -> Self { + match value { + BdkSatisfaction::Partial { + n, + m, + items, + sorted, + conditions, + } => Satisfaction::Partial { + n: n as u64, + m: m as u64, + items: items.iter().map(|e| e.to_owned() as u64).collect(), + sorted, + conditions: conditions + .into_iter() + .map(|(index, conditions)| { + ( + index as u32, + conditions.into_iter().map(|e| e.into()).collect(), + ) + }) + .collect(), + }, + BdkSatisfaction::PartialComplete { + n, + m, + items, + sorted, + conditions, + } => Satisfaction::PartialComplete { + n: n as u64, + m: m as u64, + items: items.iter().map(|e| e.to_owned() as u64).collect(), + sorted, + conditions: conditions + .into_iter() + .map(|(index, conditions)| { + ( + index.iter().map(|e| e.to_owned() as u32).collect(), + conditions.into_iter().map(|e| e.into()).collect(), + ) + }) + .collect(), + }, + BdkSatisfaction::Complete { condition } => Satisfaction::Complete { + condition: condition.into(), + }, + BdkSatisfaction::None => Satisfaction::None { + msg: "Cannot satisfy or contribute to the policy item".to_string(), + }, + } + } +} + +#[derive(Debug, Clone)] +pub struct Condition { + pub csv: Option, + pub timelock: Option, +} + +impl From for Condition { + fn from(value: BdkCondition) -> Self { + Condition { + csv: value.csv.map(|e| e.to_consensus_u32()), + timelock: value.timelock.map(|e| e.into()), + } + } +} diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 17c63536..9a3cd54b 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -1,13 +1,13 @@ use crate::bitcoin::{Psbt, Transaction}; use crate::descriptor::Descriptor; use crate::error::{ - CalculateFeeError, CannotConnectError, CreateWithPersistError, LoadWithPersistError, - SignerError, SqliteError, TxidParseError, + CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError, + LoadWithPersistError, SignerError, SqliteError, TxidParseError, }; use crate::store::Connection; use crate::types::{ AddressInfo, Balance, CanonicalTx, FullScanRequestBuilder, KeychainAndIndex, LocalOutput, - SentAndReceivedValues, SyncRequestBuilder, Update, + Policy, SentAndReceivedValues, SyncRequestBuilder, Update, }; use bitcoin_ffi::{Amount, FeeRate, OutPoint, Script}; @@ -139,6 +139,13 @@ impl Wallet { self.get_wallet().descriptor_checksum(keychain) } + pub fn policies(&self, keychain: KeychainKind) -> Result>, DescriptorError> { + self.get_wallet() + .policies(keychain) + .map_err(DescriptorError::from) + .map(|e| e.map(|p| Arc::new(p.into()))) + } + pub fn network(&self) -> Network { self.get_wallet().network() }