Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Spending Policy API #612

Closed
wants to merge 12 commits into from
59 changes: 59 additions & 0 deletions bdk-ffi/src/bdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ interface Wallet {

string descriptor_checksum(KeychainKind keychain);

[Throws=DescriptorError]
Policy? policies(KeychainKind keychain);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Balance balance();

[Throws=CannotConnectError]
Expand Down Expand Up @@ -441,6 +444,60 @@ interface Wallet {

interface Update {};

interface Policy {
string id();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only field you need/want for your use case? Just curious really, since we can expose other fields later if needed obv.

Regardless, looks good https://docs.rs/bdk_wallet/latest/bdk_wallet/descriptor/policy/struct.Policy.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other fields have been exposed now.


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<PkOrF> keys, u64 threshold);
Thresh(sequence<Policy> 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<u64> items, boolean? sorted, record<u32, sequence<Condition>> conditions);
PartialComplete(u64 n, u64 m, sequence<u64> items, boolean? sorted, record<sequence<u32>, sequence<Condition>> conditions);
Complete(Condition condition);
None(string msg);
};

dictionary Condition {
u32? csv;
LockTime? timelock;
};

interface TxBuilder {
constructor();

Expand All @@ -456,6 +513,8 @@ interface TxBuilder {

TxBuilder add_utxo(OutPoint outpoint);

TxBuilder policy_path(record<string, sequence<u64>> policy_path, KeychainKind keychain);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks!


TxBuilder change_policy(ChangeSpendPolicy change_policy);

TxBuilder do_not_spend_change();
Expand Down
6 changes: 6 additions & 0 deletions bdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
121 changes: 121 additions & 0 deletions bdk-ffi/src/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -21,6 +24,8 @@ pub struct TxBuilder {
pub(crate) recipients: Vec<(BdkScriptBuf, BdkAmount)>,
pub(crate) utxos: Vec<OutPoint>,
pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) manually_selected_only: bool,
pub(crate) fee_rate: Option<FeeRate>,
Expand All @@ -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,
Expand Down Expand Up @@ -104,6 +111,25 @@ impl TxBuilder {
})
}

pub(crate) fn policy_path(
&self,
policy_path: HashMap<String, Vec<u64>>,
keychain: KeychainKind,
) -> Arc<Self> {
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::<BTreeMap<String, Vec<usize>>>(),
);
Arc::new(updated_self)
}

pub(crate) fn change_policy(&self, change_policy: ChangeSpendPolicy) -> Arc<Self> {
Arc::new(TxBuilder {
change_policy,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<bitcoin_ffi::Script>) {}
}

#[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
}
}
Loading
Loading