Added add_foreign_utxo
To allow adding UTXOs external to the current wallet. The caller must provide the psbt::Input so we can create a coherent PSBT at the end and so this is compatible with existing PSBT workflows. Main changes: - There are now two types of UTXOs, local and foreign reflected in a `Utxo` enum. - `WeightedUtxo` now captures floating `(Utxo, usize)` tuples - `CoinSelectionResult` now has methods on it for distinguishing between local amount included vs total.
This commit is contained in:
@@ -513,11 +513,7 @@ where
|
||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||
)?;
|
||||
|
||||
let coin_selection::CoinSelectionResult {
|
||||
selected,
|
||||
selected_amount,
|
||||
mut fee_amount,
|
||||
} = coin_selection.coin_select(
|
||||
let coin_selection = coin_selection.coin_select(
|
||||
self.database.borrow().deref(),
|
||||
required_utxos,
|
||||
optional_utxos,
|
||||
@@ -525,10 +521,13 @@ where
|
||||
outgoing,
|
||||
fee_amount,
|
||||
)?;
|
||||
tx.input = selected
|
||||
let mut fee_amount = coin_selection.fee_amount;
|
||||
|
||||
tx.input = coin_selection
|
||||
.selected
|
||||
.iter()
|
||||
.map(|u| bitcoin::TxIn {
|
||||
previous_output: u.outpoint,
|
||||
previous_output: u.outpoint(),
|
||||
script_sig: Script::default(),
|
||||
sequence: n_sequence,
|
||||
witness: vec![],
|
||||
@@ -550,9 +549,8 @@ where
|
||||
Some(change_output)
|
||||
}
|
||||
};
|
||||
|
||||
let mut fee_amount = fee_amount.ceil() as u64;
|
||||
let change_val = (selected_amount - outgoing).saturating_sub(fee_amount);
|
||||
let change_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||
|
||||
match change_output {
|
||||
None if change_val.is_dust() => {
|
||||
@@ -588,14 +586,15 @@ where
|
||||
params.ordering.sort_tx(&mut tx);
|
||||
|
||||
let txid = tx.txid();
|
||||
let psbt = self.complete_transaction(tx, selected, params)?;
|
||||
let sent = coin_selection.local_selected_amount();
|
||||
let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
|
||||
|
||||
let transaction_details = TransactionDetails {
|
||||
transaction: None,
|
||||
txid,
|
||||
timestamp: time::get_timestamp(),
|
||||
received,
|
||||
sent: selected_amount,
|
||||
sent,
|
||||
fees: fee_amount,
|
||||
height: None,
|
||||
};
|
||||
@@ -705,7 +704,10 @@ where
|
||||
keychain,
|
||||
};
|
||||
|
||||
Ok((utxo, weight))
|
||||
Ok(WeightedUtxo {
|
||||
satisfaction_weight: weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -1039,18 +1041,18 @@ where
|
||||
&self,
|
||||
change_policy: tx_builder::ChangeSpendPolicy,
|
||||
unspendable: &HashSet<OutPoint>,
|
||||
manually_selected: Vec<(LocalUtxo, usize)>,
|
||||
manually_selected: Vec<WeightedUtxo>,
|
||||
must_use_all_available: bool,
|
||||
manual_only: bool,
|
||||
must_only_use_confirmed_tx: bool,
|
||||
) -> Result<(Vec<(LocalUtxo, usize)>, Vec<(LocalUtxo, usize)>), Error> {
|
||||
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
let mut may_spend = self.get_available_utxos()?;
|
||||
may_spend.retain(|may_spend| {
|
||||
manually_selected
|
||||
.iter()
|
||||
.find(|manually_selected| manually_selected.0.outpoint == may_spend.0.outpoint)
|
||||
.find(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
|
||||
.is_none()
|
||||
});
|
||||
let mut must_spend = manually_selected;
|
||||
@@ -1088,6 +1090,14 @@ where
|
||||
retain
|
||||
});
|
||||
|
||||
let mut may_spend = may_spend
|
||||
.into_iter()
|
||||
.map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(local_utxo),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if must_use_all_available {
|
||||
must_spend.append(&mut may_spend);
|
||||
}
|
||||
@@ -1098,7 +1108,7 @@ where
|
||||
fn complete_transaction(
|
||||
&self,
|
||||
tx: Transaction,
|
||||
selected: Vec<LocalUtxo>,
|
||||
selected: Vec<Utxo>,
|
||||
params: TxParams,
|
||||
) -> Result<PSBT, Error> {
|
||||
use bitcoin::util::psbt::serialize::Serialize;
|
||||
@@ -1131,9 +1141,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let lookup_output = selected
|
||||
let mut lookup_output = selected
|
||||
.into_iter()
|
||||
.map(|utxo| (utxo.outpoint, utxo))
|
||||
.map(|utxo| (utxo.outpoint(), utxo))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// add metadata for the inputs
|
||||
@@ -1142,7 +1152,7 @@ where
|
||||
.iter_mut()
|
||||
.zip(psbt.global.unsigned_tx.input.iter())
|
||||
{
|
||||
let utxo = match lookup_output.get(&input.previous_output) {
|
||||
let utxo = match lookup_output.remove(&input.previous_output) {
|
||||
Some(utxo) => utxo,
|
||||
None => continue,
|
||||
};
|
||||
@@ -1153,32 +1163,50 @@ where
|
||||
psbt_input.sighash_type = Some(sighash_type);
|
||||
}
|
||||
|
||||
// Try to find the prev_script in our db to figure out if this is internal or external,
|
||||
// and the derivation index
|
||||
let (keychain, child) = match self
|
||||
.database
|
||||
.borrow()
|
||||
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
|
||||
{
|
||||
Some(x) => x,
|
||||
None => continue,
|
||||
};
|
||||
match utxo {
|
||||
Utxo::Local(utxo) => {
|
||||
// Try to find the prev_script in our db to figure out if this is internal or external,
|
||||
// and the derivation index
|
||||
let (keychain, child) = match self
|
||||
.database
|
||||
.borrow()
|
||||
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
|
||||
{
|
||||
Some(x) => x,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let (desc, _) = self._get_descriptor_for_keychain(keychain);
|
||||
let derived_descriptor = desc.as_derived(child, &self.secp);
|
||||
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
|
||||
let desc = self.get_descriptor_for_keychain(keychain);
|
||||
let derived_descriptor = desc.as_derived(child, &self.secp);
|
||||
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
|
||||
|
||||
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
|
||||
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
|
||||
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
|
||||
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
|
||||
|
||||
let prev_output = input.previous_output;
|
||||
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
||||
if desc.is_witness() {
|
||||
psbt_input.witness_utxo =
|
||||
Some(prev_tx.output[prev_output.vout as usize].clone());
|
||||
let prev_output = input.previous_output;
|
||||
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
||||
if desc.is_witness() {
|
||||
psbt_input.witness_utxo =
|
||||
Some(prev_tx.output[prev_output.vout as usize].clone());
|
||||
}
|
||||
if !desc.is_witness() || params.force_non_witness_utxo {
|
||||
psbt_input.non_witness_utxo = Some(prev_tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !desc.is_witness() || params.force_non_witness_utxo {
|
||||
psbt_input.non_witness_utxo = Some(prev_tx);
|
||||
Utxo::Foreign {
|
||||
psbt_input: foreign_psbt_input,
|
||||
outpoint,
|
||||
} => {
|
||||
if params.force_non_witness_utxo
|
||||
&& foreign_psbt_input.non_witness_utxo.is_none()
|
||||
{
|
||||
return Err(Error::Generic(format!(
|
||||
"Missing non_witness_utxo on foreign utxo {}",
|
||||
outpoint
|
||||
)));
|
||||
}
|
||||
*psbt_input = *foreign_psbt_input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1348,7 +1376,7 @@ where
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::Network;
|
||||
use bitcoin::{util::psbt, Network};
|
||||
|
||||
use crate::database::memory::MemoryDatabase;
|
||||
use crate::database::Database;
|
||||
@@ -2237,6 +2265,182 @@ mod test {
|
||||
assert_eq!(psbt.global.unknown.get(&psbt_key), Some(&value_bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_foreign_utxo() {
|
||||
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let (wallet2, _, _) =
|
||||
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let utxo = wallet2.list_unspent().unwrap().remove(0);
|
||||
let foreign_utxo_satisfaction = wallet2
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.max_satisfaction_weight()
|
||||
.unwrap();
|
||||
|
||||
let psbt_input = psbt::Input {
|
||||
witness_utxo: Some(utxo.txout.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut builder = wallet1.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 60_000)
|
||||
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
|
||||
.unwrap();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
details.sent - details.received,
|
||||
10_000 + details.fees,
|
||||
"we should have only net spent ~10_000"
|
||||
);
|
||||
|
||||
assert!(
|
||||
psbt.global
|
||||
.unsigned_tx
|
||||
.input
|
||||
.iter()
|
||||
.find(|input| input.previous_output == utxo.outpoint)
|
||||
.is_some(),
|
||||
"foreign_utxo should be in there"
|
||||
);
|
||||
|
||||
let (psbt, finished) = wallet1.sign(psbt, None).unwrap();
|
||||
|
||||
assert!(
|
||||
!finished,
|
||||
"only one of the inputs should have been signed so far"
|
||||
);
|
||||
|
||||
let (_, finished) = wallet2.sign(psbt, None).unwrap();
|
||||
assert!(finished, "all the inputs should have been signed now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
|
||||
fn test_add_foreign_utxo_invalid_psbt_input() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let mut builder = wallet.build_tx();
|
||||
let outpoint = wallet.list_unspent().unwrap()[0].outpoint;
|
||||
let foreign_utxo_satisfaction = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.max_satisfaction_weight()
|
||||
.unwrap();
|
||||
builder
|
||||
.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
|
||||
let (wallet1, _, txid1) = get_funded_wallet(get_test_wpkh());
|
||||
let (wallet2, _, txid2) =
|
||||
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||
|
||||
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
|
||||
let tx1 = wallet1
|
||||
.database
|
||||
.borrow()
|
||||
.get_tx(&txid1, true)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.transaction
|
||||
.unwrap();
|
||||
let tx2 = wallet2
|
||||
.database
|
||||
.borrow()
|
||||
.get_tx(&txid2, true)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.transaction
|
||||
.unwrap();
|
||||
|
||||
let satisfaction_weight = wallet2
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.max_satisfaction_weight()
|
||||
.unwrap();
|
||||
|
||||
let psbt_input1 = psbt::Input {
|
||||
non_witness_utxo: Some(tx1),
|
||||
..Default::default()
|
||||
};
|
||||
let psbt_input2 = psbt::Input {
|
||||
non_witness_utxo: Some(tx2),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut builder = wallet1.build_tx();
|
||||
assert!(
|
||||
builder
|
||||
.add_foreign_utxo(utxo2.outpoint, psbt_input1, satisfaction_weight)
|
||||
.is_err(),
|
||||
"should fail when outpoint doesn't match psbt_input"
|
||||
);
|
||||
assert!(
|
||||
builder
|
||||
.add_foreign_utxo(utxo2.outpoint, psbt_input2, satisfaction_weight)
|
||||
.is_ok(),
|
||||
"shoulld be ok when outpoing does match psbt_input"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_foreign_utxo_force_non_witness_utxo() {
|
||||
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let (wallet2, _, txid2) =
|
||||
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
|
||||
|
||||
let satisfaction_weight = wallet2
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.max_satisfaction_weight()
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet1.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 60_000)
|
||||
.force_non_witness_utxo();
|
||||
|
||||
{
|
||||
let mut builder = builder.clone();
|
||||
let psbt_input = psbt::Input {
|
||||
witness_utxo: Some(utxo2.txout.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
builder
|
||||
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
|
||||
.unwrap();
|
||||
assert!(
|
||||
builder.finish().is_err(),
|
||||
"psbt_input with witness_utxo should succeed with witness_utxo"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mut builder = builder.clone();
|
||||
let tx2 = wallet2
|
||||
.database
|
||||
.borrow()
|
||||
.get_tx(&txid2, true)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.transaction
|
||||
.unwrap();
|
||||
let psbt_input = psbt::Input {
|
||||
non_witness_utxo: Some(tx2),
|
||||
..Default::default()
|
||||
};
|
||||
builder
|
||||
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
|
||||
.unwrap();
|
||||
assert!(
|
||||
builder.finish().is_ok(),
|
||||
"psbt_input with non_witness_utxo should succeed with force_non_witness_utxo"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
|
||||
|
||||
Reference in New Issue
Block a user