Write more docs, make TxBuilder::with_recipients
take Scripts
This commit is contained in:
parent
7065c1fed6
commit
eee75219e0
@ -57,7 +57,7 @@ impl AddressValidator for DummyValidator {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), magical_bitcoin_wallet::error::Error> {
|
||||
fn main() -> Result<(), magical_bitcoin_wallet::Error> {
|
||||
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
||||
let mut wallet: OfflineWallet<_> =
|
||||
Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
||||
|
@ -40,8 +40,7 @@ use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use magical_bitcoin_wallet::database::memory::MemoryDatabase;
|
||||
use magical_bitcoin_wallet::types::ScriptType;
|
||||
use magical_bitcoin_wallet::{OfflineWallet, Wallet};
|
||||
use magical_bitcoin_wallet::{OfflineWallet, Wallet, ScriptType};
|
||||
|
||||
fn main() {
|
||||
env_logger::init_from_env(
|
||||
|
@ -37,10 +37,10 @@ use log::{debug, error, info, trace, LevelFilter};
|
||||
use bitcoin::Network;
|
||||
|
||||
use magical_bitcoin_wallet::bitcoin;
|
||||
use magical_bitcoin_wallet::blockchain::ElectrumBlockchain;
|
||||
use magical_bitcoin_wallet::blockchain::compact_filters::*;
|
||||
use magical_bitcoin_wallet::cli;
|
||||
use magical_bitcoin_wallet::sled;
|
||||
use magical_bitcoin_wallet::{Client, Wallet};
|
||||
use magical_bitcoin_wallet::Wallet;
|
||||
|
||||
fn prepare_home_dir() -> PathBuf {
|
||||
let mut dir = PathBuf::new();
|
||||
@ -88,19 +88,17 @@ fn main() {
|
||||
.unwrap();
|
||||
debug!("database opened successfully");
|
||||
|
||||
let client = Client::new(
|
||||
matches.value_of("server").unwrap(),
|
||||
matches.value_of("proxy"),
|
||||
)
|
||||
.unwrap();
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
change_descriptor,
|
||||
network,
|
||||
tree,
|
||||
ElectrumBlockchain::from(client),
|
||||
)
|
||||
.unwrap();
|
||||
let num_threads = 1;
|
||||
|
||||
let mempool = Arc::new(Mempool::default());
|
||||
let peers = (0..num_threads)
|
||||
.map(|_| Peer::connect("192.168.1.136:8333", Arc::clone(&mempool), Network::Bitcoin))
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
let blockchain =
|
||||
CompactFiltersBlockchain::new(peers, "./wallet-filters", Some(500_000)).unwrap();
|
||||
|
||||
let wallet = Wallet::new(descriptor, change_descriptor, network, tree, blockchain).unwrap();
|
||||
let wallet = Arc::new(wallet);
|
||||
|
||||
if let Some(_sub_matches) = matches.subcommand_matches("repl") {
|
||||
|
@ -33,14 +33,14 @@ use log::{debug, error, info, trace, LevelFilter};
|
||||
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::{Address, OutPoint, Txid};
|
||||
use bitcoin::{Address, OutPoint, Script, Txid};
|
||||
|
||||
use crate::blockchain::log_progress;
|
||||
use crate::error::Error;
|
||||
use crate::types::ScriptType;
|
||||
use crate::{FeeRate, TxBuilder, Wallet};
|
||||
|
||||
fn parse_recipient(s: &str) -> Result<(Address, u64), String> {
|
||||
fn parse_recipient(s: &str) -> Result<(Script, u64), String> {
|
||||
let parts: Vec<_> = s.split(":").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Invalid format".to_string());
|
||||
@ -55,7 +55,7 @@ fn parse_recipient(s: &str) -> Result<(Address, u64), String> {
|
||||
return Err(format!("{:?}", e));
|
||||
}
|
||||
|
||||
Ok((addr.unwrap(), val.unwrap()))
|
||||
Ok((addr.unwrap().script_pubkey(), val.unwrap()))
|
||||
}
|
||||
|
||||
fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
|
||||
|
@ -403,12 +403,16 @@ impl From<bool> for Satisfaction {
|
||||
/// Descriptor spending policy
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Policy {
|
||||
id: String,
|
||||
/// Identifier for this policy node
|
||||
pub id: String,
|
||||
|
||||
/// Type of this policy node
|
||||
#[serde(flatten)]
|
||||
item: SatisfiableItem,
|
||||
satisfaction: Satisfaction,
|
||||
contribution: Satisfaction,
|
||||
pub item: SatisfiableItem,
|
||||
/// How a much given PSBT already satisfies this polcy node **(currently unused)**
|
||||
pub satisfaction: Satisfaction,
|
||||
/// How the wallet's descriptor can satisfy this policy node
|
||||
pub contribution: Satisfaction,
|
||||
}
|
||||
|
||||
/// An extra condition that must be satisfied but that is out of control of the user
|
||||
|
@ -153,7 +153,10 @@ mod test {
|
||||
|
||||
let addr = testutils!(@external descriptors, 10);
|
||||
wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@
|
||||
//!
|
||||
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
//! let (psbt, details) = wallet.create_tx(
|
||||
//! TxBuilder::with_recipients(vec![(to_address, 50_000)])
|
||||
//! TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
|
||||
//! .coin_selection(AlwaysSpendEverything),
|
||||
//! )?;
|
||||
//!
|
||||
|
@ -22,6 +22,51 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! Wallet export
|
||||
//!
|
||||
//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md).
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! ### Import from JSON
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use magical_bitcoin_wallet::database::*;
|
||||
//! # use magical_bitcoin_wallet::wallet::export::*;
|
||||
//! # use magical_bitcoin_wallet::*;
|
||||
//! let import = r#"{
|
||||
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
|
||||
//! "blockheight":1782088,
|
||||
//! "label":"testnet"
|
||||
//! }"#;
|
||||
//!
|
||||
//! let import = WalletExport::from_str(import)?;
|
||||
//! let wallet: OfflineWallet<_> = Wallet::new_offline(&import.descriptor(), import.change_descriptor().as_deref(), Network::Testnet, MemoryDatabase::default())?;
|
||||
//! # Ok::<_, magical_bitcoin_wallet::Error>(())
|
||||
//! ```
|
||||
//!
|
||||
//! ### Export a `Wallet`
|
||||
//! ```
|
||||
//! # use bitcoin::*;
|
||||
//! # use magical_bitcoin_wallet::database::*;
|
||||
//! # use magical_bitcoin_wallet::wallet::export::*;
|
||||
//! # use magical_bitcoin_wallet::*;
|
||||
//! let wallet: OfflineWallet<_> = Wallet::new_offline(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default()
|
||||
//! )?;
|
||||
//! let export = WalletExport::export_wallet(&wallet, "exported wallet", true)
|
||||
//! .map_err(ToString::to_string)
|
||||
//! .map_err(magical_bitcoin_wallet::Error::Generic)?;
|
||||
//!
|
||||
//! println!("Exported: {}", export.to_string());
|
||||
//! # Ok::<_, magical_bitcoin_wallet::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -32,10 +77,15 @@ use crate::blockchain::Blockchain;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Structure that contains the export of a wallet
|
||||
///
|
||||
/// For a usage example see [this module](crate::wallet::export)'s documentation.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WalletExport {
|
||||
descriptor: String,
|
||||
/// Earliest block to rescan when looking for the wallet's transactions
|
||||
pub blockheight: u32,
|
||||
/// Arbitrary label for the wallet
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
@ -54,6 +104,17 @@ impl FromStr for WalletExport {
|
||||
}
|
||||
|
||||
impl WalletExport {
|
||||
/// Export a wallet
|
||||
///
|
||||
/// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
|
||||
/// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44
|
||||
/// and others.
|
||||
///
|
||||
/// If `include_blockheight` is `true`, this function will look into the `wallet`'s database
|
||||
/// for the oldest transaction it knows and use that as the earliest block to rescan.
|
||||
///
|
||||
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
|
||||
/// returned will be `0`.
|
||||
pub fn export_wallet<B: Blockchain, D: BatchDatabase>(
|
||||
wallet: &Wallet<B, D>,
|
||||
label: &str,
|
||||
@ -118,10 +179,12 @@ impl WalletExport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the external descriptor
|
||||
pub fn descriptor(&self) -> String {
|
||||
self.descriptor.clone()
|
||||
}
|
||||
|
||||
/// Return the internal descriptor, if present
|
||||
pub fn change_descriptor(&self) -> Option<String> {
|
||||
let replaced = self.descriptor.replace("/0/*", "/1/*");
|
||||
|
||||
|
@ -229,7 +229,7 @@ where
|
||||
(None, Some(csv)) => csv,
|
||||
(Some(rbf), Some(csv)) if rbf < csv => return Err(Error::Generic(format!("Cannot enable RBF with nSequence `{}`, since at least `{}` is required to spend with OP_CSV", rbf, csv))),
|
||||
(None, _) if requirements.timelock.is_some() => 0xFFFFFFFE,
|
||||
(Some(rbf), _) if rbf >= 0xFFFFFFFE => return Err(Error::Generic("Cannot enable RBF with anumber >= 0xFFFFFFFE".into())),
|
||||
(Some(rbf), _) if rbf >= 0xFFFFFFFE => return Err(Error::Generic("Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into())),
|
||||
(Some(rbf), _) => rbf,
|
||||
(None, _) => 0xFFFFFFFF,
|
||||
};
|
||||
@ -254,22 +254,19 @@ where
|
||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
||||
fee_amount += calc_fee_bytes(tx.get_weight());
|
||||
|
||||
for (index, (address, satoshi)) in builder.recipients.iter().enumerate() {
|
||||
for (index, (script_pubkey, satoshi)) in builder.recipients.iter().enumerate() {
|
||||
let value = match builder.send_all {
|
||||
true => 0,
|
||||
false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
|
||||
false => *satoshi,
|
||||
};
|
||||
|
||||
// TODO: proper checks for testnet/regtest p2sh/p2pkh
|
||||
if address.network != self.network && self.network != Network::Regtest {
|
||||
return Err(Error::InvalidAddressNetwork(address.clone()));
|
||||
} else if self.is_mine(&address.script_pubkey())? {
|
||||
if self.is_mine(script_pubkey)? {
|
||||
received += value;
|
||||
}
|
||||
|
||||
let new_out = TxOut {
|
||||
script_pubkey: address.script_pubkey(),
|
||||
script_pubkey: script_pubkey.clone(),
|
||||
value,
|
||||
};
|
||||
fee_amount += calc_fee_bytes(serialize(&new_out).len() * 4);
|
||||
@ -1251,7 +1248,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(0))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(0))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@ -1263,7 +1260,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(1))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(1))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@ -1272,7 +1269,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(42))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(42))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.version, 42);
|
||||
@ -1283,7 +1280,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.lock_time, 0);
|
||||
@ -1294,7 +1294,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.lock_time, 100_000);
|
||||
@ -1305,7 +1308,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(630_000))
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000);
|
||||
@ -1316,7 +1321,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(630_000))
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000);
|
||||
@ -1330,7 +1337,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(50000))
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(50000),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@ -1339,7 +1348,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 6);
|
||||
@ -1350,7 +1362,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFD);
|
||||
@ -1364,7 +1378,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf_with_sequence(3))
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
|
||||
.enable_rbf_with_sequence(3),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@ -1373,20 +1390,23 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Cannot enable RBF with anumber >= 0xFFFFFFFE")]
|
||||
#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")]
|
||||
fn test_create_tx_invalid_rbf_sequence() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr, 25_000)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
|
||||
.enable_rbf_with_sequence(0xFFFFFFFE),
|
||||
)
|
||||
.unwrap();
|
||||
@ -1398,7 +1418,7 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr, 25_000)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
|
||||
.enable_rbf_with_sequence(0xDEADBEEF),
|
||||
)
|
||||
.unwrap();
|
||||
@ -1411,7 +1431,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF);
|
||||
@ -1426,7 +1449,8 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 25_000)]).do_not_spend_change(),
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
|
||||
.do_not_spend_change(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@ -1438,7 +1462,11 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 25_000), (addr, 10_000)]).send_all(),
|
||||
TxBuilder::with_recipients(vec![
|
||||
(addr.script_pubkey(), 25_000),
|
||||
(addr.script_pubkey(), 10_000),
|
||||
])
|
||||
.send_all(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@ -1448,7 +1476,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
|
||||
@ -1463,7 +1491,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature);
|
||||
@ -1475,7 +1503,7 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
.send_all(),
|
||||
)
|
||||
@ -1492,7 +1520,7 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 25_000)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
|
||||
.ordering(TxOrdering::Untouched),
|
||||
)
|
||||
.unwrap();
|
||||
@ -1510,7 +1538,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 49_800)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
49_800,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
|
||||
@ -1525,7 +1556,7 @@ mod test {
|
||||
// very high fee rate, so that the only output would be below dust
|
||||
wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.send_all()
|
||||
.fee_rate(crate::FeeRate::from_sat_per_vb(453.0)),
|
||||
)
|
||||
@ -1538,8 +1569,11 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 30_000), (addr.clone(), 10_000)])
|
||||
.ordering(super::tx_builder::TxOrdering::BIP69Lexicographic),
|
||||
TxBuilder::with_recipients(vec![
|
||||
(addr.script_pubkey(), 30_000),
|
||||
(addr.script_pubkey(), 10_000),
|
||||
])
|
||||
.ordering(super::tx_builder::TxOrdering::BIP69Lexicographic),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -1557,7 +1591,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 30_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
30_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.inputs[0].sighash_type, Some(bitcoin::SigHashType::All));
|
||||
@ -1569,7 +1606,7 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 30_000)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)])
|
||||
.sighash(bitcoin::SigHashType::Single),
|
||||
)
|
||||
.unwrap();
|
||||
@ -1588,7 +1625,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1);
|
||||
@ -1612,7 +1649,7 @@ mod test {
|
||||
|
||||
let addr = testutils!(@external descriptors, 5);
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1);
|
||||
@ -1633,7 +1670,7 @@ mod test {
|
||||
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@ -1656,7 +1693,7 @@ mod test {
|
||||
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt.inputs[0].redeem_script, None);
|
||||
@ -1679,7 +1716,7 @@ mod test {
|
||||
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
let script = Script::from(
|
||||
@ -1699,7 +1736,7 @@ mod test {
|
||||
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
||||
@ -1712,7 +1749,7 @@ mod test {
|
||||
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
assert!(psbt.inputs[0].non_witness_utxo.is_none());
|
||||
@ -1726,7 +1763,7 @@ mod test {
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.force_non_witness_utxo()
|
||||
.send_all(),
|
||||
)
|
||||
@ -1742,7 +1779,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, mut details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
let tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -1759,7 +1799,10 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, mut details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]))
|
||||
.create_tx(TxBuilder::with_recipients(vec![(
|
||||
addr.script_pubkey(),
|
||||
25_000,
|
||||
)]))
|
||||
.unwrap();
|
||||
let tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -1777,7 +1820,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, mut details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
let tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -1798,7 +1843,9 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 25_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -1860,7 +1907,7 @@ mod test {
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.send_all()
|
||||
.enable_rbf(),
|
||||
)
|
||||
@ -1914,7 +1961,7 @@ mod test {
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.utxos(vec![OutPoint {
|
||||
txid: incoming_txid,
|
||||
vout: 0,
|
||||
@ -1961,7 +2008,9 @@ mod test {
|
||||
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -2025,7 +2074,7 @@ mod test {
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.clone(), 0)])
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
|
||||
.send_all()
|
||||
.add_utxo(OutPoint {
|
||||
txid: incoming_txid,
|
||||
@ -2101,7 +2150,9 @@ mod test {
|
||||
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tx = psbt.extract_tx();
|
||||
assert_eq!(tx.input.len(), 1);
|
||||
@ -2161,7 +2212,9 @@ mod test {
|
||||
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let (psbt, mut original_details) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf())
|
||||
.create_tx(
|
||||
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@ -2226,7 +2279,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
@ -2242,7 +2295,7 @@ mod test {
|
||||
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
@ -2257,7 +2310,7 @@ mod test {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_new_address().unwrap();
|
||||
let (mut psbt, _) = wallet
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all())
|
||||
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
|
||||
.unwrap();
|
||||
|
||||
psbt.inputs[0].hd_keypaths.clear();
|
||||
|
@ -22,6 +22,72 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! Generalized signers
|
||||
//!
|
||||
//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet)
|
||||
//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function.
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bitcoin::util::psbt;
|
||||
//! # use bitcoin::util::bip32::Fingerprint;
|
||||
//! # use magical_bitcoin_wallet::signer::*;
|
||||
//! # use magical_bitcoin_wallet::database::*;
|
||||
//! # use magical_bitcoin_wallet::*;
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct CustomHSM;
|
||||
//! # impl CustomHSM {
|
||||
//! # fn sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn connect() -> Self {
|
||||
//! # CustomHSM
|
||||
//! # }
|
||||
//! # }
|
||||
//! #[derive(Debug)]
|
||||
//! struct CustomSigner {
|
||||
//! device: CustomHSM,
|
||||
//! }
|
||||
//!
|
||||
//! impl CustomSigner {
|
||||
//! fn connect() -> Self {
|
||||
//! CustomSigner { device: CustomHSM::connect() }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl Signer for CustomSigner {
|
||||
//! fn sign(
|
||||
//! &self,
|
||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||
//! input_index: Option<usize>,
|
||||
//! ) -> Result<(), SignerError> {
|
||||
//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?;
|
||||
//! self.device.sign_input(psbt, input_index)?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn sign_whole_tx(&self) -> bool {
|
||||
//! false
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! let custom_signer = CustomSigner::connect();
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
//! let mut wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! wallet.add_signer(
|
||||
//! ScriptType::External,
|
||||
//! Fingerprint::from_str("e30f11b8").unwrap().into(),
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(Box::new(custom_signer))
|
||||
//! );
|
||||
//!
|
||||
//! # Ok::<_, magical_bitcoin_wallet::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
@ -42,7 +108,7 @@ use miniscript::{Legacy, MiniscriptKey, Segwitv0};
|
||||
use crate::descriptor::XKeyUtils;
|
||||
|
||||
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
|
||||
/// many of them
|
||||
/// multiple of them
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SignerId<Pk: MiniscriptKey> {
|
||||
PkHash(<Pk as MiniscriptKey>::Hash),
|
||||
@ -93,15 +159,30 @@ impl fmt::Display for SignerError {
|
||||
impl std::error::Error for SignerError {}
|
||||
|
||||
/// Trait for signers
|
||||
///
|
||||
/// This trait can be implemented to provide customized signers to the wallet. For an example see
|
||||
/// [`this module`](crate::wallet::signer)'s documentation.
|
||||
pub trait Signer: fmt::Debug {
|
||||
/// Sign a PSBT
|
||||
///
|
||||
/// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole
|
||||
/// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and
|
||||
/// can be ignored.
|
||||
fn sign(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: Option<usize>,
|
||||
) -> Result<(), SignerError>;
|
||||
|
||||
/// Return whether or not the signer signs the whole transaction in one go instead of every
|
||||
/// input individually
|
||||
fn sign_whole_tx(&self) -> bool;
|
||||
|
||||
/// Return the secret key for the signer
|
||||
///
|
||||
/// This is used internally to reconstruct the original descriptor that may contain secrets.
|
||||
/// External signers that are meant to keep key isolated should just return `None` here (which
|
||||
/// is the default for this method, if not overridden).
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
None
|
||||
}
|
||||
@ -195,6 +276,11 @@ impl Signer for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the order in which signers are called
|
||||
///
|
||||
/// The default value is `100`. Signers with an ordering above that will be called later,
|
||||
/// and they will thus see the partial signatures added to the transaction once they get to sign
|
||||
/// themselves.
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub struct SignerOrdering(pub usize);
|
||||
|
||||
|
@ -22,6 +22,14 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! Cross-platform time
|
||||
//!
|
||||
//! This module provides a function to get the current timestamp that works on all the platforms
|
||||
//! supported by the library.
|
||||
//!
|
||||
//! It can be useful to compare it with the timestamps found in
|
||||
//! [`TransactionDetails`](crate::types::TransactionDetails).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@ -29,6 +37,7 @@ use js_sys::Date;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::{Instant as SystemInstant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Return the current timestamp in seconds
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn get_timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
@ -36,6 +45,7 @@ pub fn get_timestamp() -> u64 {
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
/// Return the current timestamp in seconds
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_timestamp() -> u64 {
|
||||
let millis = Date::now();
|
||||
|
@ -22,17 +22,40 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! Transaction builder
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use magical_bitcoin_wallet::*;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi, with a custom fee rate
|
||||
//! // of 5.0 satoshi/vbyte, only spending non-change outputs and with RBF signaling
|
||||
//! // enabled
|
||||
//! let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
//! .do_not_spend_change()
|
||||
//! .enable_rbf();
|
||||
//! ```
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::default::Default;
|
||||
|
||||
use bitcoin::{Address, OutPoint, SigHashType, Transaction};
|
||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use crate::types::{FeeRate, UTXO};
|
||||
|
||||
/// A transaction builder
|
||||
///
|
||||
/// This structure contains the configuration that the wallet must follow to build a transaction.
|
||||
///
|
||||
/// For an example see [this module](super::tx_builder)'s documentation;
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TxBuilder<Cs: CoinSelectionAlgorithm> {
|
||||
pub(crate) recipients: Vec<(Address, u64)>,
|
||||
pub(crate) recipients: Vec<(Script, u64)>,
|
||||
pub(crate) send_all: bool,
|
||||
pub(crate) fee_rate: Option<FeeRate>,
|
||||
pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||
@ -49,112 +72,182 @@ pub struct TxBuilder<Cs: CoinSelectionAlgorithm> {
|
||||
}
|
||||
|
||||
impl TxBuilder<DefaultCoinSelectionAlgorithm> {
|
||||
/// Create an empty builder
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_recipients(recipients: Vec<(Address, u64)>) -> Self {
|
||||
/// Create a builder starting from a list of recipients
|
||||
pub fn with_recipients(recipients: Vec<(Script, u64)>) -> Self {
|
||||
Self::default().set_recipients(recipients)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Cs: CoinSelectionAlgorithm> TxBuilder<Cs> {
|
||||
pub fn set_recipients(mut self, recipients: Vec<(Address, u64)>) -> Self {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(mut self, recipients: Vec<(Script, u64)>) -> Self {
|
||||
self.recipients = recipients;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_recipient(mut self, address: Address, amount: u64) -> Self {
|
||||
self.recipients.push((address, amount));
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(mut self, script_pubkey: Script, amount: u64) -> Self {
|
||||
self.recipients.push((script_pubkey, amount));
|
||||
self
|
||||
}
|
||||
|
||||
/// Send all the selected utxos to a single output
|
||||
///
|
||||
/// Adding more than one recipients with this option enabled will result in an error.
|
||||
///
|
||||
/// The value associated with the only recipient is irrelevant and will be replaced by the wallet.
|
||||
pub fn send_all(mut self) -> Self {
|
||||
self.send_all = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a custom fee rate
|
||||
pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
|
||||
self.fee_rate = Some(fee_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the policy path to use while creating the transaction
|
||||
///
|
||||
/// This method accepts a map where the key is the policy node id (see
|
||||
/// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of
|
||||
/// the items that are intended to be satisfied from the policy node (see
|
||||
/// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)).
|
||||
pub fn policy_path(mut self, policy_path: BTreeMap<String, Vec<usize>>) -> Self {
|
||||
self.policy_path = Some(policy_path);
|
||||
self
|
||||
}
|
||||
|
||||
/// These have priority over the "unspendable" utxos
|
||||
/// Replace the internal list of utxos that **must** be spent with a new list
|
||||
///
|
||||
/// 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.
|
||||
pub fn utxos(mut self, utxos: Vec<OutPoint>) -> Self {
|
||||
self.utxos = Some(utxos);
|
||||
self
|
||||
}
|
||||
|
||||
/// This has priority over the "unspendable" utxos
|
||||
/// Add a utxo 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.
|
||||
pub fn add_utxo(mut self, utxo: OutPoint) -> Self {
|
||||
self.utxos.get_or_insert(vec![]).push(utxo);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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::utxos`] and
|
||||
/// [`TxBuilder::add_utxo`] have priority over these. See the docs of the two linked methods
|
||||
/// for more details.
|
||||
pub fn unspendable(mut self, unspendable: Vec<OutPoint>) -> Self {
|
||||
self.unspendable = Some(unspendable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a utxo to the internal list of unspendable utxos
|
||||
///
|
||||
/// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::utxos`] and
|
||||
/// [`TxBuilder::add_utxo`] have priority over this. See the docs of the two linked methods
|
||||
/// for more details.
|
||||
pub fn add_unspendable(mut self, unspendable: OutPoint) -> Self {
|
||||
self.unspendable.get_or_insert(vec![]).push(unspendable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sign with a specific sig hash
|
||||
///
|
||||
/// **Use this option very carefully**
|
||||
pub fn sighash(mut self, sighash: SigHashType) -> Self {
|
||||
self.sighash = Some(sighash);
|
||||
self
|
||||
}
|
||||
|
||||
/// Choose the ordering for inputs and outputs of the transaction
|
||||
pub fn ordering(mut self, ordering: TxOrdering) -> Self {
|
||||
self.ordering = ordering;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a specific nLockTime while creating the transaction
|
||||
///
|
||||
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
||||
pub fn nlocktime(mut self, locktime: u32) -> Self {
|
||||
self.locktime = Some(locktime);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
pub fn enable_rbf(self) -> Self {
|
||||
self.enable_rbf_with_sequence(0xFFFFFFFD)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn enable_rbf_with_sequence(mut self, nsequence: u32) -> Self {
|
||||
self.rbf = Some(nsequence);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build a transaction with a specific version
|
||||
///
|
||||
/// The `version` should always be greater than `0` and greater than `1` if the wallet's
|
||||
/// descriptors contain an "older" (OP_CSV) operator.
|
||||
pub fn version(mut self, version: u32) -> Self {
|
||||
self.version = Some(Version(version));
|
||||
self
|
||||
}
|
||||
|
||||
/// Do not spend change outputs
|
||||
///
|
||||
/// This effectively adds all the change outputs to the "unspendable" list. See
|
||||
/// [`TxBuilder::unspendable`].
|
||||
pub fn do_not_spend_change(mut self) -> Self {
|
||||
self.change_policy = ChangeSpendPolicy::ChangeForbidden;
|
||||
self
|
||||
}
|
||||
|
||||
/// Only spend change outputs
|
||||
///
|
||||
/// This effectively adds all the non-change outputs to the "unspendable" list. See
|
||||
/// [`TxBuilder::unspendable`].
|
||||
pub fn only_spend_change(mut self) -> Self {
|
||||
self.change_policy = ChangeSpendPolicy::OnlyChange;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and
|
||||
/// [`TxBuilder::only_spend_change`] for some shortcuts.
|
||||
pub fn change_policy(mut self, change_policy: ChangeSpendPolicy) -> Self {
|
||||
self.change_policy = change_policy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit
|
||||
/// descriptors.
|
||||
///
|
||||
/// This is useful for signers which always require it, like Trezor hardware wallets.
|
||||
pub fn force_non_witness_utxo(mut self) -> Self {
|
||||
self.force_non_witness_utxo = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Choose the coin selection algorithm
|
||||
///
|
||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(self, coin_selection: P) -> TxBuilder<P> {
|
||||
TxBuilder {
|
||||
recipients: self.recipients,
|
||||
@ -175,10 +268,14 @@ impl<Cs: CoinSelectionAlgorithm> TxBuilder<Cs> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordering of the transaction's inputs and outputs
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum TxOrdering {
|
||||
/// Randomized (default)
|
||||
Shuffle,
|
||||
/// Unchanged
|
||||
Untouched,
|
||||
/// BIP69 / Lexicographic
|
||||
BIP69Lexicographic,
|
||||
}
|
||||
|
||||
@ -215,7 +312,9 @@ impl TxOrdering {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper type that wraps u32 and has a default value of 1
|
||||
/// Transaction version
|
||||
///
|
||||
/// Has a default value of `1`
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub(crate) struct Version(pub(crate) u32);
|
||||
|
||||
@ -225,10 +324,14 @@ impl Default for Version {
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy regarding the use of change outputs when creating a transaction
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum ChangeSpendPolicy {
|
||||
/// Use both change and non-change outputs (default)
|
||||
ChangeAllowed,
|
||||
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
|
||||
OnlyChange,
|
||||
/// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`])
|
||||
ChangeForbidden,
|
||||
}
|
||||
|
||||
|
@ -307,7 +307,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr, 25_000)])).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
@ -334,7 +334,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr, 25_000)])).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
@ -373,7 +373,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..5 {
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 5_000)])).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)])).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
@ -405,7 +405,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 5_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
@ -437,7 +437,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
@ -470,7 +470,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
@ -501,7 +501,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
Loading…
x
Reference in New Issue
Block a user