diff --git a/splits.yaml b/splits.yaml new file mode 100644 index 0000000000..ef7d4c1547 --- /dev/null +++ b/splits.yaml @@ -0,0 +1,15 @@ +# example split file + +# output fields: +# address: output recipient bitcoin address +# value: output bitcoin value (optional, defaults to minimal-non dust value for `address`) +# runes: output rune value map (values respect rune divisibility) +outputs: +- address: bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297 + value: 10 sat + runes: + UNCOMMON•GOODS: 1234 + GRIEF•WAGE: 5000000 +- address: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy + runes: + HELLO•WORLD: 22.5 diff --git a/src/index.rs b/src/index.rs index d1260359a1..4cd24d3aec 100644 --- a/src/index.rs +++ b/src/index.rs @@ -684,7 +684,7 @@ impl Index { } pub fn export(&self, filename: &String, include_addresses: bool) -> Result { - let mut writer = BufWriter::new(fs::File::create(filename)?); + let mut writer = BufWriter::new(File::create(filename)?); let rtx = self.database.begin_read()?; let blocks_indexed = rtx diff --git a/src/inscriptions/media.rs b/src/inscriptions/media.rs index cdccbf99ab..a6ebbd97e7 100644 --- a/src/inscriptions/media.rs +++ b/src/inscriptions/media.rs @@ -5,7 +5,6 @@ use { self, BROTLI_MODE_FONT as FONT, BROTLI_MODE_GENERIC as GENERIC, BROTLI_MODE_TEXT as TEXT, }, mp4::{MediaType, Mp4Reader, TrackType}, - std::{fs::File, io::BufReader}, }; #[derive(Debug, PartialEq, Copy, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 9fd522800c..60bc129e51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ use { consensus::{self, Decodable, Encodable}, hash_types::{BlockHash, TxMerkleNode}, hashes::Hash, + policy::MAX_STANDARD_TX_WEIGHT, script, transaction::Version, Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, @@ -52,7 +53,7 @@ use { clap::{ArgGroup, Parser}, error::{ResultExt, SnafuError}, html_escaper::{Escape, Trusted}, - http::HeaderMap, + http::{HeaderMap, StatusCode}, lazy_static::lazy_static, ordinals::{ varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, @@ -70,8 +71,8 @@ use { env, ffi::OsString, fmt::{self, Display, Formatter}, - fs, - io::{self, Cursor, Read}, + fs::{self, File}, + io::{self, BufReader, Cursor, Read}, mem, net::ToSocketAddrs, path::{Path, PathBuf}, diff --git a/src/settings.rs b/src/settings.rs index 7cc5ffb1ff..2fe46fad32 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -74,7 +74,7 @@ impl Settings { }; let config = if let Some(config_path) = config_path { - serde_yaml::from_reader(fs::File::open(&config_path).context(anyhow!( + serde_yaml::from_reader(File::open(&config_path).context(anyhow!( "failed to open config file `{}`", config_path.display() ))?) @@ -994,7 +994,7 @@ mod tests { #[test] fn example_config_file_is_valid() { - let _: Settings = serde_yaml::from_reader(fs::File::open("ord.yaml").unwrap()).unwrap(); + let _: Settings = serde_yaml::from_reader(File::open("ord.yaml").unwrap()).unwrap(); } #[test] diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index 2e28d6e8dc..2c5f5438b4 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -77,7 +77,7 @@ impl Decode { .bitcoin_rpc_client(None)? .get_raw_transaction(&txid, None)? } else if let Some(file) = self.file { - Transaction::consensus_decode(&mut io::BufReader::new(fs::File::open(file)?))? + Transaction::consensus_decode(&mut io::BufReader::new(File::open(file)?))? } else { Transaction::consensus_decode(&mut io::BufReader::new(io::stdin()))? }; diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs index 1e38a8a576..897082f13c 100644 --- a/src/subcommand/env.rs +++ b/src/subcommand/env.rs @@ -203,7 +203,7 @@ rpcport={bitcoind_port} } serde_json::to_writer_pretty( - fs::File::create(self.directory.join("env.json"))?, + File::create(self.directory.join("env.json"))?, &Info { bitcoind_port, ord_port, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 00775aa52c..8e38038d06 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -25,6 +25,7 @@ pub mod sats; pub mod send; mod shared_args; pub mod sign; +pub mod split; pub mod transactions; #[derive(Debug, Parser)] @@ -85,6 +86,8 @@ pub(crate) enum Subcommand { Send(send::Send), #[command(about = "Sign message")] Sign(sign::Sign), + #[command(about = "Split outputs")] + Split(split::Split), #[command(about = "See wallet transactions")] Transactions(transactions::Transactions), } @@ -131,6 +134,7 @@ impl WalletCommand { Subcommand::Sats(sats) => sats.run(wallet), Subcommand::Send(send) => send.run(wallet), Subcommand::Sign(sign) => sign.run(wallet), + Subcommand::Split(split) => split.run(wallet), Subcommand::Transactions(transactions) => transactions.run(wallet), } } diff --git a/src/subcommand/wallet/batch_command.rs b/src/subcommand/wallet/batch_command.rs index 5da34d6348..43e4c79133 100644 --- a/src/subcommand/wallet/batch_command.rs +++ b/src/subcommand/wallet/batch_command.rs @@ -6,7 +6,8 @@ pub(crate) struct Batch { shared: SharedArgs, #[arg( long, - help = "Inscribe multiple inscriptions and rune defined in YAML ." + help = "Inscribe multiple inscriptions and rune defined in YAML .", + value_name = "BATCH_FILE" )] pub(crate) batch: PathBuf, } diff --git a/src/subcommand/wallet/burn.rs b/src/subcommand/wallet/burn.rs index 2268784652..9c05c752eb 100644 --- a/src/subcommand/wallet/burn.rs +++ b/src/subcommand/wallet/burn.rs @@ -44,7 +44,8 @@ impl Burn { self.fee_rate, )?; - let (txid, psbt, fee) = wallet.sign_transaction(unsigned_transaction, self.dry_run)?; + let (txid, psbt, fee) = + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; Ok(Some(Box::new(send::Output { txid, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 6a69a71935..f02643d361 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -110,7 +110,7 @@ impl Inscribe { Ok(Some(cbor)) } else if let Some(path) = json { let value: serde_json::Value = - serde_json::from_reader(fs::File::open(path)?).context("failed to parse JSON metadata")?; + serde_json::from_reader(File::open(path)?).context("failed to parse JSON metadata")?; let mut cbor = Vec::new(); ciborium::into_writer(&value, &mut cbor)?; diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index dc26a57fc3..da9785ccc5 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -73,7 +73,8 @@ impl Send { )?, }; - let (txid, psbt, fee) = wallet.sign_transaction(unsigned_transaction, self.dry_run)?; + let (txid, psbt, fee) = + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; Ok(Some(Box::new(Output { txid, @@ -213,16 +214,16 @@ impl Send { } inputs.push(output); - } - } - if input_rune_balances - .get(&spaced_rune.rune) - .cloned() - .unwrap_or_default() - >= amount - { - break; + if input_rune_balances + .get(&spaced_rune.rune) + .cloned() + .unwrap_or_default() + >= amount + { + break; + } + } } } diff --git a/src/subcommand/wallet/shared_args.rs b/src/subcommand/wallet/shared_args.rs index e9db3b5cb7..d5f6c4d2fb 100644 --- a/src/subcommand/wallet/shared_args.rs +++ b/src/subcommand/wallet/shared_args.rs @@ -18,7 +18,10 @@ pub(super) struct SharedArgs { #[arg( long, alias = "nolimit", - help = "Do not check that transactions are equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently nonstandard and will not be relayed by bitcoind in its default configuration. Do not use this flag unless you understand the implications." + help = "Allow transactions larger than MAX_STANDARD_TX_WEIGHT of 400,000 weight units and \ + OP_RETURNs greater than 83 bytes. Transactions over this limit are nonstandard and will not be \ + relayed by bitcoind in its default configuration. Do not use this flag unless you understand \ + the implications." )] pub(crate) no_limit: bool, } diff --git a/src/subcommand/wallet/split.rs b/src/subcommand/wallet/split.rs new file mode 100644 index 0000000000..333a909ec1 --- /dev/null +++ b/src/subcommand/wallet/split.rs @@ -0,0 +1,1423 @@ +use {super::*, splitfile::Splitfile}; + +mod splitfile; + +const MAX_STANDARD_OP_RETURN_SIZE: usize = 83; + +#[derive(Debug, PartialEq)] +enum Error { + DustOutput { + value: Amount, + threshold: Amount, + output: usize, + }, + DustPostage { + value: Amount, + threshold: Amount, + }, + NoOutputs, + RunestoneSize { + size: usize, + }, + Shortfall { + rune: SpacedRune, + have: Pile, + need: Pile, + }, + ZeroValue { + output: usize, + rune: SpacedRune, + }, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::DustOutput { + value, + threshold, + output, + } => write!( + f, + "output {output} value {value} below dust threshold {threshold}" + ), + Self::DustPostage { value, threshold } => { + write!(f, "postage value {value} below dust threshold {threshold}") + } + Self::NoOutputs => write!(f, "split file must contain at least one output"), + Self::RunestoneSize { size } => write!( + f, + "runestone size {size} over maximum standard OP_RETURN size {MAX_STANDARD_OP_RETURN_SIZE}" + ), + Self::Shortfall { rune, have, need } => { + write!(f, "wallet contains {have} of {rune} but need {need}") + } + Self::ZeroValue { output, rune } => { + write!(f, "output {output} has zero value for rune {rune}") + } + } + } +} + +impl std::error::Error for Error {} + +#[derive(Debug, Parser)] +pub(crate) struct Split { + #[arg(long, help = "Don't sign or broadcast transaction")] + pub(crate) dry_run: bool, + #[arg(long, help = "Use fee rate of sats/vB")] + fee_rate: FeeRate, + #[arg( + long, + help = "Include postage with change output. [default: 10000 sat]", + value_name = "AMOUNT" + )] + pub(crate) postage: Option, + #[arg( + long, + help = "Split outputs multiple inscriptions and rune defined in YAML .", + value_name = "SPLIT_FILE" + )] + pub(crate) splits: PathBuf, + #[arg( + long, + alias = "nolimit", + help = "Allow OP_RETURN greater than 83 bytes. Transactions over this limit are nonstandard \ + and will not be relayed by bitcoind in its default configuration. Do not use this flag unless \ + you understand the implications." + )] + pub(crate) no_limit: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Output { + pub txid: Txid, + pub psbt: String, + pub fee: u64, +} + +impl Split { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + ensure!( + wallet.has_rune_index(), + "`ord wallet split` requires index created with `--index-runes`", + ); + + wallet.lock_non_cardinal_outputs()?; + + let splits = Splitfile::load(&self.splits, &wallet)?; + + let inscribed_outputs = wallet + .inscriptions() + .keys() + .map(|satpoint| satpoint.outpoint) + .collect::>(); + + let balances = wallet + .get_runic_outputs()? + .into_iter() + .filter(|output| !inscribed_outputs.contains(output)) + .map(|output| { + wallet.get_runes_balances_in_output(&output).map(|balance| { + ( + output, + balance + .into_iter() + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) + .collect(), + ) + }) + }) + .collect::>>>()?; + + let unfunded_transaction = Self::build_transaction( + self.no_limit, + balances, + &wallet.get_change_address()?, + self.postage, + &splits, + )?; + + let unsigned_transaction = fund_raw_transaction( + wallet.bitcoin_client(), + self.fee_rate, + &unfunded_transaction, + )?; + + let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; + + let (txid, psbt, fee) = + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; + + Ok(Some(Box::new(Output { txid, psbt, fee }))) + } + + fn build_transaction( + no_runestone_limit: bool, + balances: BTreeMap>, + change_address: &Address, + postage: Option, + splits: &Splitfile, + ) -> Result { + if splits.outputs.is_empty() { + return Err(Error::NoOutputs); + } + + let postage = postage.unwrap_or(TARGET_POSTAGE); + + let change_script_pubkey = change_address.script_pubkey(); + + let change_dust_threshold = change_script_pubkey.minimal_non_dust(); + + if postage < change_script_pubkey.minimal_non_dust() { + return Err(Error::DustPostage { + value: postage, + threshold: change_dust_threshold, + }); + } + + let mut input_runes_required = BTreeMap::::new(); + + for (i, output) in splits.outputs.iter().enumerate() { + for (&rune, &amount) in &output.runes { + if amount == 0 { + return Err(Error::ZeroValue { + rune: splits.rune_info[&rune].spaced_rune, + output: i, + }); + } + let required = input_runes_required.entry(rune).or_default(); + *required = (*required).checked_add(amount).unwrap(); + } + } + + let mut input_rune_balances: BTreeMap = BTreeMap::new(); + + let mut inputs = Vec::new(); + + for (output, runes) in balances { + for (rune, required) in &input_runes_required { + if input_rune_balances.get(rune).copied().unwrap_or_default() >= *required { + continue; + } + + if runes.get(rune).copied().unwrap_or_default() == 0 { + continue; + } + + for (rune, balance) in &runes { + *input_rune_balances.entry(*rune).or_default() += balance; + } + + inputs.push(output); + + break; + } + } + + for (&rune, &need) in &input_runes_required { + let have = input_rune_balances.get(&rune).copied().unwrap_or_default(); + if have < need { + let info = splits.rune_info[&rune]; + return Err(Error::Shortfall { + rune: info.spaced_rune, + have: Pile { + amount: have, + divisibility: info.divisibility, + symbol: info.symbol, + }, + need: Pile { + amount: need, + divisibility: info.divisibility, + symbol: info.symbol, + }, + }); + } + } + + let mut need_rune_change_output = false; + for (rune, input) in input_rune_balances { + if input > input_runes_required.get(&rune).copied().unwrap_or_default() { + need_rune_change_output = true; + } + } + + let mut edicts = Vec::new(); + + let base = if need_rune_change_output { 2 } else { 1 }; + + for (i, output) in splits.outputs.iter().enumerate() { + for (rune, amount) in &output.runes { + edicts.push(Edict { + id: splits.rune_info.get(rune).unwrap().id, + amount: *amount, + output: (i + base).try_into().unwrap(), + }); + } + } + + let runestone = Runestone { + edicts, + ..default() + }; + + let mut output = Vec::new(); + + let runestone_script_pubkey = runestone.encipher(); + let size = runestone_script_pubkey.len(); + + if !no_runestone_limit && size > MAX_STANDARD_OP_RETURN_SIZE { + return Err(Error::RunestoneSize { size }); + } + + output.push(TxOut { + script_pubkey: runestone_script_pubkey, + value: Amount::from_sat(0), + }); + + if need_rune_change_output { + output.push(TxOut { + script_pubkey: change_script_pubkey, + value: postage, + }); + } + + for (i, split_output) in splits.outputs.iter().enumerate() { + let script_pubkey = split_output.address.script_pubkey(); + let threshold = script_pubkey.minimal_non_dust(); + let value = split_output.value.unwrap_or(threshold); + if value < threshold { + return Err(Error::DustOutput { + output: i, + threshold, + value, + }); + } + output.push(TxOut { + script_pubkey, + value, + }); + } + + let tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: inputs + .into_iter() + .map(|previous_output| TxIn { + previous_output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }) + .collect(), + output, + }; + + for output in &tx.output { + assert!(output.value >= output.script_pubkey.minimal_non_dust()); + } + + assert_eq!( + Runestone::decipher(&tx), + Some(Artifact::Runestone(runestone)), + ); + + Ok(tx) + } +} + +#[cfg(test)] +mod tests { + use {super::*, splitfile::RuneInfo}; + + #[test] + fn splits_must_have_at_least_one_output() { + assert_eq!( + Split::build_transaction( + false, + BTreeMap::new(), + &change(0), + None, + &Splitfile { + outputs: Vec::new(), + rune_info: BTreeMap::new(), + }, + ) + .unwrap_err(), + Error::NoOutputs, + ); + } + + #[test] + fn postage_may_not_be_dust() { + assert_eq!( + Split::build_transaction( + false, + BTreeMap::new(), + &change(0), + Some(Amount::from_sat(100)), + &Splitfile { + outputs: vec![splitfile::Output { + address: address(0), + runes: [(Rune(0), 1000)].into(), + value: Some(Amount::from_sat(1000)), + }], + rune_info: BTreeMap::new(), + }, + ) + .unwrap_err(), + Error::DustPostage { + value: Amount::from_sat(100), + threshold: Amount::from_sat(294), + }, + ); + } + + #[test] + fn output_rune_value_may_not_be_zero() { + assert_eq!( + Split::build_transaction( + false, + BTreeMap::new(), + &change(0), + None, + &Splitfile { + outputs: vec![splitfile::Output { + address: address(0), + runes: [(Rune(0), 0)].into(), + value: Some(Amount::from_sat(1000)), + }], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 10, + symbol: Some('@'), + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 1, + }, + }, + )] + .into() + }, + ) + .unwrap_err(), + Error::ZeroValue { + output: 0, + rune: SpacedRune { + rune: Rune(0), + spacers: 1, + }, + }, + ); + + assert_eq!( + Split::build_transaction( + false, + BTreeMap::new(), + &change(0), + None, + &Splitfile { + outputs: vec![ + splitfile::Output { + address: address(0), + runes: [(Rune(0), 100)].into(), + value: Some(Amount::from_sat(1000)), + }, + splitfile::Output { + address: address(0), + runes: [(Rune(0), 0)].into(), + value: Some(Amount::from_sat(1000)), + }, + ], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 10, + symbol: Some('@'), + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 10, + }, + }, + )] + .into() + }, + ) + .unwrap_err(), + Error::ZeroValue { + output: 1, + rune: SpacedRune { + rune: Rune(0), + spacers: 10, + }, + }, + ); + } + + #[test] + fn wallet_must_have_enough_runes() { + assert_eq!( + Split::build_transaction( + false, + BTreeMap::new(), + &change(0), + None, + &Splitfile { + outputs: vec![splitfile::Output { + address: address(0), + runes: [(Rune(0), 1000)].into(), + value: Some(Amount::from_sat(1000)), + }], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 10, + symbol: Some('@'), + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 2, + }, + }, + )] + .into(), + }, + ) + .unwrap_err(), + Error::Shortfall { + rune: SpacedRune { + rune: Rune(0), + spacers: 2 + }, + have: Pile { + amount: 0, + divisibility: 10, + symbol: Some('@'), + }, + need: Pile { + amount: 1000, + divisibility: 10, + symbol: Some('@'), + }, + }, + ); + + assert_eq!( + Split::build_transaction( + false, + [(outpoint(0), [(Rune(0), 1000)].into())].into(), + &change(0), + None, + &Splitfile { + outputs: vec![splitfile::Output { + address: address(0), + runes: [(Rune(0), 2000)].into(), + value: Some(Amount::from_sat(1000)), + }], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 2, + symbol: Some('x'), + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 1 + }, + }, + )] + .into() + }, + ) + .unwrap_err(), + Error::Shortfall { + rune: SpacedRune { + rune: Rune(0), + spacers: 1, + }, + have: Pile { + amount: 1000, + divisibility: 2, + symbol: Some('x'), + }, + need: Pile { + amount: 2000, + divisibility: 2, + symbol: Some('x'), + }, + }, + ); + } + + #[test] + fn split_output_values_may_not_be_dust() { + assert_eq!( + Split::build_transaction( + false, + [(outpoint(0), [(Rune(0), 1000)].into())].into(), + &change(0), + None, + &Splitfile { + outputs: vec![splitfile::Output { + address: address(0), + runes: [(Rune(0), 1000)].into(), + value: Some(Amount::from_sat(1)), + }], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }, + ) + .unwrap_err(), + Error::DustOutput { + value: Amount::from_sat(1), + threshold: Amount::from_sat(294), + output: 0, + } + ); + + assert_eq!( + Split::build_transaction( + false, + [(outpoint(0), [(Rune(0), 2000)].into())].into(), + &change(0), + None, + &Splitfile { + outputs: vec![ + splitfile::Output { + address: address(0), + runes: [(Rune(0), 1000)].into(), + value: Some(Amount::from_sat(1000)), + }, + splitfile::Output { + address: address(0), + runes: [(Rune(0), 1000)].into(), + value: Some(Amount::from_sat(10)), + }, + ], + rune_info: [( + Rune(0), + RuneInfo { + id: RuneId { block: 1, tx: 1 }, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into() + }, + ) + .unwrap_err(), + Error::DustOutput { + value: Amount::from_sat(10), + threshold: Amount::from_sat(294), + output: 1, + } + ); + } + + #[test] + fn one_output_no_change() { + let address = address(0); + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + + let balances = [(output, [(rune, 1000)].into())].into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 1000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 1 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn one_output_with_change_for_outgoing_rune_with_default_postage() { + let address = address(0); + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + let change = change(0); + + let balances = [(output, [(rune, 2000)].into())].into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 1000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 2 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: change.into(), + value: TARGET_POSTAGE, + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn one_output_with_change_for_outgoing_rune_with_non_default_postage() { + let address = address(0); + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + let change = change(0); + + let balances = [(output, [(rune, 2000)].into())].into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 1000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction( + false, + balances, + &change, + Some(Amount::from_sat(500)), + &splits, + ) + .unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 2 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: change.into(), + value: Amount::from_sat(500), + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn one_output_with_change_for_non_outgoing_rune() { + let address = address(0); + let output = outpoint(0); + let change = change(0); + + let balances = [(output, [(Rune(0), 1000), (Rune(1), 1000)].into())].into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(Rune(0), 1000)].into(), + value: None, + }], + rune_info: [( + Rune(0), + RuneInfo { + id: rune_id(0), + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id: rune_id(0), + amount: 1000, + output: 2 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: change.into(), + value: TARGET_POSTAGE, + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn outputs_without_value_use_correct_dust_amount() { + let address = "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297" + .parse::>() + .unwrap() + .assume_checked(); + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + + let balances = [(output, [(rune, 1000)].into())].into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 1000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 1 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(330), + } + ], + }, + ); + } + + #[test] + fn excessive_inputs_are_not_selected() { + let address = address(0); + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + + let balances = [ + (output, [(rune, 1000)].into()), + (outpoint(1), [(rune, 1000)].into()), + ] + .into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 1000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 1 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn multiple_inputs_may_be_selected() { + let address = address(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + + let balances = [ + (outpoint(0), [(rune, 1000)].into()), + (outpoint(1), [(rune, 1000)].into()), + ] + .into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(rune, 2000)].into(), + value: None, + }], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + previous_output: outpoint(0), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + TxIn { + previous_output: outpoint(1), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + ], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![Edict { + id, + amount: 2000, + output: 1 + }], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn two_outputs_no_change() { + let output = outpoint(0); + let rune = Rune(0); + let id = RuneId { block: 1, tx: 1 }; + + let balances = [(output, [(rune, 1000)].into())].into(); + + let splits = Splitfile { + outputs: vec![ + splitfile::Output { + address: address(0), + runes: [(rune, 500)].into(), + value: None, + }, + splitfile::Output { + address: address(1), + runes: [(rune, 500)].into(), + value: None, + }, + ], + rune_info: [( + rune, + RuneInfo { + id, + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: output, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![ + Edict { + id, + amount: 500, + output: 1 + }, + Edict { + id, + amount: 500, + output: 2 + } + ], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address(0).into(), + value: Amount::from_sat(294), + }, + TxOut { + script_pubkey: address(1).into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn outputs_may_receive_multiple_runes() { + let address = address(0); + + let balances = [ + (outpoint(0), [(Rune(0), 1000)].into()), + (outpoint(1), [(Rune(1), 2000)].into()), + ] + .into(); + + let splits = Splitfile { + outputs: vec![splitfile::Output { + address: address.clone(), + runes: [(Rune(0), 1000), (Rune(1), 2000)].into(), + value: None, + }], + rune_info: [ + ( + Rune(0), + RuneInfo { + id: rune_id(0), + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + ), + ( + Rune(1), + RuneInfo { + id: rune_id(1), + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(1), + spacers: 0, + }, + }, + ), + ] + .into(), + }; + + let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap(); + + pretty_assert_eq!( + tx, + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + previous_output: outpoint(0), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + TxIn { + previous_output: outpoint(1), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + ], + output: vec![ + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: vec![ + Edict { + id: rune_id(0), + amount: 1000, + output: 1 + }, + Edict { + id: rune_id(1), + amount: 2000, + output: 1 + }, + ], + etching: None, + mint: None, + pointer: None, + } + .encipher() + }, + TxOut { + script_pubkey: address.into(), + value: Amount::from_sat(294), + } + ], + }, + ); + } + + #[test] + fn oversize_op_return_is_an_error() { + let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into(); + + let splits = Splitfile { + outputs: (0..10) + .map(|i| splitfile::Output { + address: address(i).clone(), + runes: [(Rune(0), 1_000_000_000)].into(), + value: None, + }) + .collect(), + rune_info: [( + Rune(0), + RuneInfo { + id: rune_id(0), + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + assert_eq!( + Split::build_transaction(false, balances, &change(0), None, &splits).unwrap_err(), + Error::RunestoneSize { size: 85 }, + ); + } + + #[test] + fn oversize_op_return_is_allowed_with_flag() { + let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into(); + + let splits = Splitfile { + outputs: (0..10) + .map(|i| splitfile::Output { + address: address(i).clone(), + runes: [(Rune(0), 1_000_000_000)].into(), + value: None, + }) + .collect(), + rune_info: [( + Rune(0), + RuneInfo { + id: rune_id(0), + divisibility: 0, + symbol: None, + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + }, + )] + .into(), + }; + + pretty_assert_eq!( + Split::build_transaction(true, balances, &change(0), None, &splits).unwrap(), + Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: outpoint(0), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: (0..11) + .map(|i| if i == 0 { + TxOut { + value: Amount::from_sat(0), + script_pubkey: Runestone { + edicts: (0..10) + .map(|i| Edict { + id: rune_id(0), + amount: 1_000_000_000, + output: i + 1, + }) + .collect(), + etching: None, + mint: None, + pointer: None, + } + .encipher(), + } + } else { + TxOut { + script_pubkey: address(i - 1).into(), + value: Amount::from_sat(294), + } + }) + .collect() + } + ); + } +} diff --git a/src/subcommand/wallet/split/splitfile.rs b/src/subcommand/wallet/split/splitfile.rs new file mode 100644 index 0000000000..751b542bf4 --- /dev/null +++ b/src/subcommand/wallet/split/splitfile.rs @@ -0,0 +1,96 @@ +use super::*; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct SplitfileUnchecked { + outputs: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct OutputUnchecked { + address: Address, + value: Option>, + runes: BTreeMap, +} + +pub(crate) struct Splitfile { + pub(crate) outputs: Vec, + pub(crate) rune_info: BTreeMap, +} + +pub(crate) struct Output { + pub(crate) address: Address, + pub(crate) value: Option, + pub(crate) runes: BTreeMap, +} + +#[derive(Clone, Copy)] +pub(crate) struct RuneInfo { + pub(crate) divisibility: u8, + pub(crate) id: RuneId, + pub(crate) spaced_rune: SpacedRune, + pub(crate) symbol: Option, +} + +impl Splitfile { + pub(crate) fn load(path: &Path, wallet: &Wallet) -> Result { + let network = wallet.chain().network(); + + let unchecked = Self::load_unchecked(path)?; + + let mut rune_info = BTreeMap::::new(); + + let mut outputs = Vec::new(); + + for output in unchecked.outputs { + let mut runes = BTreeMap::new(); + + for (spaced_rune, decimal) in output.runes { + let info = if let Some(info) = rune_info.get(&spaced_rune.rune) { + info + } else { + let (id, entry, _parent) = wallet + .get_rune(spaced_rune.rune)? + .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?; + rune_info.insert( + spaced_rune.rune, + RuneInfo { + divisibility: entry.divisibility, + id, + spaced_rune: entry.spaced_rune, + symbol: entry.symbol, + }, + ); + rune_info.get(&spaced_rune.rune).unwrap() + }; + + let amount = decimal.to_integer(info.divisibility)?; + + runes.insert(spaced_rune.rune, amount); + } + + outputs.push(Output { + address: output.address.require_network(network)?, + value: output.value.map(|DeserializeFromStr(value)| value), + runes, + }); + } + + Ok(Self { outputs, rune_info }) + } + + fn load_unchecked(path: &Path) -> Result { + Ok(serde_yaml::from_reader(File::open(path)?)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn example_split_file_is_valid() { + Splitfile::load_unchecked("splits.yaml".as_ref()).unwrap(); + } +} diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index b0d2a00871..e69e8329ee 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -90,7 +90,7 @@ mod tests { inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), id: inscription_id(1), number: 1, - output: Some(tx_out(1, address())), + output: Some(tx_out(1, address(0))), satpoint: satpoint(1, 0), ..default() }, @@ -122,7 +122,7 @@ mod tests { inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), id: inscription_id(1), number: 1, - output: Some(tx_out(1, address())), + output: Some(tx_out(1, address(0))), sat: Some(Sat(1)), satpoint: satpoint(1, 0), ..default() @@ -154,7 +154,7 @@ mod tests { id: inscription_id(2), next: Some(inscription_id(3)), number: 1, - output: Some(tx_out(1, address())), + output: Some(tx_out(1, address(0))), previous: Some(inscription_id(1)), satpoint: satpoint(1, 0), ..default() @@ -180,7 +180,7 @@ mod tests { inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), id: inscription_id(2), number: -1, - output: Some(tx_out(1, address())), + output: Some(tx_out(1, address(0))), satpoint: SatPoint { outpoint: unbound_outpoint(), offset: 0 diff --git a/src/test.rs b/src/test.rs index f4aa2c1176..748fec9990 100644 --- a/src/test.rs +++ b/src/test.rs @@ -12,7 +12,11 @@ pub(crate) use { unindent::Unindent, }; -pub(crate) fn txid(n: u64) -> Txid { +pub(crate) fn rune_id(tx: u32) -> RuneId { + RuneId { block: 1, tx } +} + +pub(crate) fn txid(n: u32) -> Txid { let hex = format!("{n:x}"); if hex.is_empty() || hex.len() > 1 { @@ -22,22 +26,37 @@ pub(crate) fn txid(n: u64) -> Txid { hex.repeat(64).parse().unwrap() } -pub(crate) fn outpoint(n: u64) -> OutPoint { - format!("{}:{}", txid(n), n).parse().unwrap() +pub(crate) fn outpoint(n: u32) -> OutPoint { + OutPoint { + txid: txid(n), + vout: n, + } } -pub(crate) fn satpoint(n: u64, offset: u64) -> SatPoint { +pub(crate) fn satpoint(n: u32, offset: u64) -> SatPoint { SatPoint { - outpoint: outpoint(n), offset, + outpoint: outpoint(n), } } -pub(crate) fn address() -> Address { - "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" - .parse::>() - .unwrap() - .assume_checked() +pub(crate) fn address(n: u32) -> Address { + match n { + 0 => "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + 1 => "bc1qhl452zcq3ng5kzajzkx9jnzncml9tnsk3w96s6", + 2 => "bc1qqqcjq9jydx79rywltc38g5qfrjq485a8xfmkf7", + 3 => "bc1qcq2uv5nk6hec6kvag3wyevp6574qmsm9scjxc2", + 4 => "bc1qukgekwq8e68ay0mewdrvg0d3cfuc094aj2rvx9", + 5 => "bc1qtdjs8tgkaja5ddxs0j7rn52uqfdtqa53mum8xc", + 6 => "bc1qd3ex6kwlc5ett55hgsnk94y8q2zhdyxyqyujkl", + 7 => "bc1q8dcv8r903evljd87mcg0hq8lphclch7pd776wt", + 8 => "bc1q9j6xvm3td447ygnhfra5tfkpkcupwe9937nhjq", + 9 => "bc1qlyrhjzvxdzmvxe2mnr37p68vkl5fysyhfph8z0", + _ => panic!(), + } + .parse::>() + .unwrap() + .assume_checked() } pub(crate) fn recipient() -> ScriptBuf { diff --git a/src/wallet.rs b/src/wallet.rs index 37abace28e..0ee430a5c5 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -290,10 +290,12 @@ impl Wallet { ) .send()?; - if !response.status().is_success() { + if response.status() == StatusCode::NOT_FOUND { return Ok(None); } + let response = response.error_for_status()?; + let rune_json: api::Rune = serde_json::from_str(&response.text()?)?; Ok(Some((rune_json.id, rune_json.entry, rune_json.parent))) @@ -749,7 +751,7 @@ impl Wallet { ) } - pub(super) fn sign_transaction( + pub(super) fn sign_and_broadcast_transaction( &self, unsigned_transaction: Transaction, dry_run: bool, diff --git a/src/wallet/batch.rs b/src/wallet/batch.rs index 665dc989ac..1d775cebbe 100644 --- a/src/wallet/batch.rs +++ b/src/wallet/batch.rs @@ -4,7 +4,6 @@ use { blockdata::{opcodes, script}, key::PrivateKey, key::{TapTweak, TweakedKeypair, TweakedPublicKey, UntweakedKeypair}, - policy::MAX_STANDARD_TX_WEIGHT, secp256k1::{self, constants::SCHNORR_SIGNATURE_SIZE, rand, Secp256k1, XOnlyPublicKey}, sighash::{Prevouts, SighashCache, TapSighashType}, taproot::Signature, @@ -74,7 +73,7 @@ mod tests { #[test] fn reveal_transaction_pays_fee() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let utxos = vec![(outpoint(1), tx_out(20000, address(0)))]; let inscription = inscription("text/plain", "ord"); let commit_address = change(0); let reveal_address = recipient_address(); @@ -120,7 +119,7 @@ mod tests { #[test] fn inscribe_transactions_opt_in_to_rbf() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let utxos = vec![(outpoint(1), tx_out(20000, address(0)))]; let inscription = inscription("text/plain", "ord"); let commit_address = change(0); let reveal_address = recipient_address(); @@ -160,7 +159,7 @@ mod tests { #[test] fn inscribe_with_no_satpoint_and_no_cardinal_utxos() { - let utxos = vec![(outpoint(1), tx_out(1000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1000, address(0)))]; let mut inscriptions = BTreeMap::new(); inscriptions.insert( SatPoint { @@ -210,8 +209,8 @@ mod tests { #[test] fn inscribe_with_no_satpoint_and_enough_cardinal_utxos() { let utxos = vec![ - (outpoint(1), tx_out(20_000, address())), - (outpoint(2), tx_out(20_000, address())), + (outpoint(1), tx_out(20_000, address(0))), + (outpoint(2), tx_out(20_000, address(0))), ]; let mut inscriptions = BTreeMap::new(); inscriptions.insert( @@ -255,8 +254,8 @@ mod tests { #[test] fn inscribe_with_custom_fee_rate() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(20_000, address(0))), ]; let mut inscriptions = BTreeMap::new(); inscriptions.insert( @@ -330,8 +329,8 @@ mod tests { #[test] fn inscribe_with_parent() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(20_000, address(0))), ]; let mut inscriptions = BTreeMap::new(); @@ -428,8 +427,8 @@ mod tests { #[test] fn inscribe_with_commit_fee_rate() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(20_000, address(0))), ]; let mut inscriptions = BTreeMap::new(); inscriptions.insert( @@ -503,7 +502,7 @@ mod tests { #[test] fn inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))]; let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); let satpoint = None; @@ -544,7 +543,7 @@ mod tests { #[test] fn inscribe_with_no_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))]; let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); let satpoint = None; @@ -581,8 +580,8 @@ mod tests { #[test] fn batch_inscribe_with_parent() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(50_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(50_000, address(0))), ]; let parent = inscription_id(1); @@ -690,12 +689,12 @@ mod tests { #[test] fn batch_inscribe_satpoints_with_parent() { let utxos = vec![ - (outpoint(1), tx_out(1_111, address())), - (outpoint(2), tx_out(2_222, address())), - (outpoint(3), tx_out(3_333, address())), - (outpoint(4), tx_out(10_000, address())), - (outpoint(5), tx_out(50_000, address())), - (outpoint(6), tx_out(60_000, address())), + (outpoint(1), tx_out(1_111, address(0))), + (outpoint(2), tx_out(2_222, address(0))), + (outpoint(3), tx_out(3_333, address(0))), + (outpoint(4), tx_out(10_000, address(0))), + (outpoint(5), tx_out(50_000, address(0))), + (outpoint(6), tx_out(60_000, address(0))), ]; let parent = inscription_id(1); @@ -822,8 +821,8 @@ mod tests { #[test] fn batch_inscribe_with_parent_not_enough_cardinals_utxos_fails() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(20_000, address(0))), ]; let parent = inscription_id(1); @@ -899,8 +898,8 @@ mod tests { #[should_panic(expected = "invariant: shared-output has only one destination")] fn batch_inscribe_with_inconsistent_reveal_addresses_panics() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(80_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(80_000, address(0))), ]; let parent = inscription_id(1); @@ -968,7 +967,7 @@ mod tests { #[test] fn batch_inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))]; let wallet_inscriptions = BTreeMap::new(); @@ -1016,8 +1015,8 @@ mod tests { #[test] fn batch_inscribe_into_separate_outputs() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(80_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(80_000, address(0))), ]; let wallet_inscriptions = BTreeMap::new(); @@ -1073,8 +1072,8 @@ mod tests { #[test] fn batch_inscribe_into_separate_outputs_with_parent() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(50_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(50_000, address(0))), ]; let parent = inscription_id(1); diff --git a/src/wallet/transaction_builder.rs b/src/wallet/transaction_builder.rs index eb411c1df0..4e97eee01b 100644 --- a/src/wallet/transaction_builder.rs +++ b/src/wallet/transaction_builder.rs @@ -727,9 +727,9 @@ mod tests { #[test] fn select_sat() { let mut utxos = vec![ - (outpoint(1), tx_out(5_000, address())), - (outpoint(2), tx_out(49 * COIN_VALUE, address())), - (outpoint(3), tx_out(2_000, address())), + (outpoint(1), tx_out(5_000, address(0))), + (outpoint(2), tx_out(49 * COIN_VALUE, address(0))), + (outpoint(3), tx_out(2_000, address(0))), ]; let tx_builder = TransactionBuilder::new( @@ -765,9 +765,9 @@ mod tests { #[test] fn tx_builder_to_transaction() { let mut amounts = BTreeMap::new(); - amounts.insert(outpoint(1), tx_out(5_000, address())); - amounts.insert(outpoint(2), tx_out(5_000, address())); - amounts.insert(outpoint(3), tx_out(2_000, address())); + amounts.insert(outpoint(1), tx_out(5_000, address(0))); + amounts.insert(outpoint(2), tx_out(5_000, address(0))); + amounts.insert(outpoint(3), tx_out(2_000, address(0))); let tx_builder = TransactionBuilder { amounts, @@ -816,7 +816,7 @@ mod tests { #[test] fn transactions_are_rbf() { - let utxos = vec![(outpoint(1), tx_out(5_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))]; assert!(TransactionBuilder::new( satpoint(1, 0), @@ -837,7 +837,7 @@ mod tests { #[test] fn deduct_fee() { - let utxos = vec![(outpoint(1), tx_out(5_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -865,7 +865,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: deducting fee does not consume sat")] fn invariant_deduct_fee_does_not_consume_sat() { - let utxos = vec![(outpoint(1), tx_out(5_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))]; TransactionBuilder::new( satpoint(1, 4_950), @@ -889,8 +889,8 @@ mod tests { #[test] fn additional_postage_added_when_required() { let utxos = vec![ - (outpoint(1), tx_out(5_000, address())), - (outpoint(2), tx_out(5_000, address())), + (outpoint(1), tx_out(5_000, address(0))), + (outpoint(2), tx_out(5_000, address(0))), ]; pretty_assert_eq!( @@ -918,7 +918,7 @@ mod tests { #[test] fn insufficient_padding_to_add_postage_no_utxos() { - let utxos = vec![(outpoint(1), tx_out(5_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -941,8 +941,8 @@ mod tests { #[test] fn insufficient_padding_to_add_postage_small_utxos() { let utxos = vec![ - (outpoint(1), tx_out(5_000, address())), - (outpoint(2), tx_out(1, address())), + (outpoint(1), tx_out(5_000, address(0))), + (outpoint(2), tx_out(1, address(0))), ]; pretty_assert_eq!( @@ -966,8 +966,8 @@ mod tests { #[test] fn excess_additional_postage_is_stripped() { let utxos = vec![ - (outpoint(1), tx_out(5_000, address())), - (outpoint(2), tx_out(25_000, address())), + (outpoint(1), tx_out(5_000, address(0))), + (outpoint(2), tx_out(25_000, address(0))), ]; pretty_assert_eq!( @@ -1003,7 +1003,7 @@ mod tests { TransactionBuilder::new( satpoint(2, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(4, address()))] + vec![(outpoint(1), tx_out(4, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1024,7 +1024,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 4), BTreeMap::new(), - vec![(outpoint(1), tx_out(4, address()))] + vec![(outpoint(1), tx_out(4, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1045,7 +1045,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 2), BTreeMap::new(), - vec![(outpoint(1), tx_out(5, address()))] + vec![(outpoint(1), tx_out(5, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1066,7 +1066,7 @@ mod tests { let mut builder = TransactionBuilder::new( satpoint(1, 2), BTreeMap::new(), - vec![(outpoint(1), tx_out(5, address()))] + vec![(outpoint(1), tx_out(5, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1095,7 +1095,7 @@ mod tests { let mut builder = TransactionBuilder::new( satpoint(1, 2), BTreeMap::new(), - vec![(outpoint(1), tx_out(5, address()))] + vec![(outpoint(1), tx_out(5, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1116,7 +1116,7 @@ mod tests { #[test] fn excess_postage_is_stripped() { - let utxos = vec![(outpoint(1), tx_out(1_000_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -1147,7 +1147,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: excess postage is stripped")] fn invariant_excess_postage_is_stripped() { - let utxos = vec![(outpoint(1), tx_out(1_000_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))]; TransactionBuilder::new( satpoint(1, 0), @@ -1169,7 +1169,7 @@ mod tests { #[test] fn sat_is_aligned() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -1197,8 +1197,8 @@ mod tests { #[test] fn alignment_output_under_dust_limit_is_padded() { let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(10_000, address())), + (outpoint(1), tx_out(10_000, address(0))), + (outpoint(2), tx_out(10_000, address(0))), ]; pretty_assert_eq!( @@ -1230,7 +1230,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: all outputs are either change or recipient")] fn invariant_all_output_are_recognized() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; let mut builder = TransactionBuilder::new( satpoint(1, 3_333), @@ -1260,7 +1260,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: all outputs are above dust limit")] fn invariant_all_output_are_above_dust_limit() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; TransactionBuilder::new( satpoint(1, 1), @@ -1288,7 +1288,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: sat is at first position in recipient output")] fn invariant_sat_is_aligned() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; TransactionBuilder::new( satpoint(1, 3_333), @@ -1313,7 +1313,7 @@ mod tests { #[test] #[should_panic(expected = "invariant: fee estimation is correct")] fn invariant_fee_is_at_least_target_fee_rate() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; TransactionBuilder::new( satpoint(1, 0), @@ -1338,9 +1338,9 @@ mod tests { #[should_panic(expected = "invariant: recipient address appears exactly once in outputs")] fn invariant_recipient_appears_exactly_once() { let mut amounts = BTreeMap::new(); - amounts.insert(outpoint(1), tx_out(5_000, address())); - amounts.insert(outpoint(2), tx_out(5_000, address())); - amounts.insert(outpoint(3), tx_out(2_000, address())); + amounts.insert(outpoint(1), tx_out(5_000, address(0))); + amounts.insert(outpoint(2), tx_out(5_000, address(0))); + amounts.insert(outpoint(3), tx_out(2_000, address(0))); TransactionBuilder { amounts, @@ -1379,9 +1379,9 @@ mod tests { #[should_panic(expected = "invariant: change addresses appear at most once in outputs")] fn invariant_change_appears_at_most_once() { let mut amounts = BTreeMap::new(); - amounts.insert(outpoint(1), tx_out(5_000, address())); - amounts.insert(outpoint(2), tx_out(5_000, address())); - amounts.insert(outpoint(3), tx_out(2_000, address())); + amounts.insert(outpoint(1), tx_out(5_000, address(0))); + amounts.insert(outpoint(2), tx_out(5_000, address(0))); + amounts.insert(outpoint(3), tx_out(2_000, address(0))); TransactionBuilder { amounts, @@ -1419,8 +1419,8 @@ mod tests { #[test] fn do_not_select_already_inscribed_sats_for_cardinal_utxos() { let utxos = vec![ - (outpoint(1), tx_out(100, address())), - (outpoint(2), tx_out(49 * COIN_VALUE, address())), + (outpoint(1), tx_out(100, address(0))), + (outpoint(2), tx_out(49 * COIN_VALUE, address(0))), ]; pretty_assert_eq!( @@ -1444,8 +1444,8 @@ mod tests { #[test] fn do_not_select_runic_utxos_for_cardinal_utxos() { let utxos = vec![ - (outpoint(1), tx_out(100, address())), - (outpoint(2), tx_out(49 * COIN_VALUE, address())), + (outpoint(1), tx_out(100, address(0))), + (outpoint(2), tx_out(49 * COIN_VALUE, address(0))), ]; pretty_assert_eq!( @@ -1468,7 +1468,7 @@ mod tests { #[test] fn do_not_send_two_inscriptions_at_once() { - let utxos = vec![(outpoint(1), tx_out(1_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -1494,7 +1494,7 @@ mod tests { #[test] fn build_transaction_with_custom_fee_rate() { - let utxos = vec![(outpoint(1), tx_out(10_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))]; let fee_rate = FeeRate::try_from(17.3).unwrap(); @@ -1529,7 +1529,7 @@ mod tests { #[test] fn exact_transaction_has_correct_value() { - let utxos = vec![(outpoint(1), tx_out(5_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -1557,8 +1557,8 @@ mod tests { #[test] fn exact_transaction_adds_output_to_cover_value() { let utxos = vec![ - (outpoint(1), tx_out(1_000, address())), - (outpoint(2), tx_out(1_000, address())), + (outpoint(1), tx_out(1_000, address(0))), + (outpoint(2), tx_out(1_000, address(0))), ]; pretty_assert_eq!( @@ -1586,7 +1586,7 @@ mod tests { #[test] fn refuse_to_send_dust() { - let utxos = vec![(outpoint(1), tx_out(1_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1_000, address(0)))]; pretty_assert_eq!( TransactionBuilder::new( @@ -1612,8 +1612,8 @@ mod tests { #[test] fn do_not_select_outputs_which_do_not_pay_for_their_own_fee_at_default_fee_rate() { let utxos = vec![ - (outpoint(1), tx_out(1_000, address())), - (outpoint(2), tx_out(100, address())), + (outpoint(1), tx_out(1_000, address(0))), + (outpoint(2), tx_out(100, address(0))), ]; pretty_assert_eq!( @@ -1637,8 +1637,8 @@ mod tests { #[test] fn do_not_select_outputs_which_do_not_pay_for_their_own_fee_at_higher_fee_rate() { let utxos = vec![ - (outpoint(1), tx_out(1_000, address())), - (outpoint(2), tx_out(500, address())), + (outpoint(1), tx_out(1_000, address(0))), + (outpoint(2), tx_out(500, address(0))), ]; pretty_assert_eq!( @@ -1689,7 +1689,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(1_000, address()))] + vec![(outpoint(1), tx_out(1_000, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1716,7 +1716,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(20_099, address()))] + vec![(outpoint(1), tx_out(20_099, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1743,7 +1743,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(1_500, address()))] + vec![(outpoint(1), tx_out(1_500, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1770,7 +1770,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(1_500, address()))] + vec![(outpoint(1), tx_out(1_500, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1792,7 +1792,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(1000, address()))] + vec![(outpoint(1), tx_out(1000, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1814,7 +1814,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(1000, address()))] + vec![(outpoint(1), tx_out(1000, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1836,7 +1836,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(2000, address()))] + vec![(outpoint(1), tx_out(2000, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1863,7 +1863,7 @@ mod tests { TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), - vec![(outpoint(1), tx_out(45000, address()))] + vec![(outpoint(1), tx_out(45000, address(0)))] .into_iter() .collect(), BTreeSet::new(), @@ -1887,12 +1887,12 @@ mod tests { #[test] fn select_outgoing_can_select_multiple_utxos() { let mut utxos = vec![ - (outpoint(2), tx_out(3_006, address())), // 2. biggest utxo is selected 2nd leaving us needing 4206 more - (outpoint(1), tx_out(3_003, address())), // 1. satpoint is selected 1st leaving us needing 7154 more - (outpoint(5), tx_out(3_004, address())), - (outpoint(4), tx_out(3_001, address())), // 4. smallest utxo >= 1259 is selected 4th, filling deficit - (outpoint(3), tx_out(3_005, address())), // 3. next biggest utxo is selected 3rd leaving us needing 1259 more - (outpoint(6), tx_out(3_002, address())), + (outpoint(2), tx_out(3_006, address(0))), // 2. biggest utxo is selected 2nd leaving us needing 4206 more + (outpoint(1), tx_out(3_003, address(0))), // 1. satpoint is selected 1st leaving us needing 7154 more + (outpoint(5), tx_out(3_004, address(0))), + (outpoint(4), tx_out(3_001, address(0))), // 4. smallest utxo >= 1259 is selected 4th, filling deficit + (outpoint(3), tx_out(3_005, address(0))), // 3. next biggest utxo is selected 3rd leaving us needing 1259 more + (outpoint(6), tx_out(3_002, address(0))), ]; let tx_builder = TransactionBuilder::new( @@ -1936,13 +1936,13 @@ mod tests { #[test] fn pad_alignment_output_can_select_multiple_utxos() { let mut utxos = vec![ - (outpoint(4), tx_out(101, address())), // 4. smallest utxo >= 84 is selected 4th, filling deficit - (outpoint(1), tx_out(20_000, address())), // 1. satpoint is selected 1st leaving deficit 293 - (outpoint(2), tx_out(105, address())), // 2. biggest utxo <= 293 is selected 2nd leaving deficit 188 - (outpoint(5), tx_out(103, address())), - (outpoint(6), tx_out(10_000, address())), - (outpoint(3), tx_out(104, address())), // 3. biggest utxo <= 188 is selected 3rd leaving deficit 84 - (outpoint(7), tx_out(102, address())), + (outpoint(4), tx_out(101, address(0))), // 4. smallest utxo >= 84 is selected 4th, filling deficit + (outpoint(1), tx_out(20_000, address(0))), // 1. satpoint is selected 1st leaving deficit 293 + (outpoint(2), tx_out(105, address(0))), // 2. biggest utxo <= 293 is selected 2nd leaving deficit 188 + (outpoint(5), tx_out(103, address(0))), + (outpoint(6), tx_out(10_000, address(0))), + (outpoint(3), tx_out(104, address(0))), // 3. biggest utxo <= 188 is selected 3rd leaving deficit 84 + (outpoint(7), tx_out(102, address(0))), ]; let tx_builder = TransactionBuilder::new( @@ -1996,13 +1996,13 @@ mod tests { expected_value: Amount, ) { let utxos = vec![ - (outpoint(4), tx_out(101, address())), - (outpoint(1), tx_out(20_000, address())), - (outpoint(2), tx_out(105, address())), - (outpoint(5), tx_out(103, address())), - (outpoint(6), tx_out(10_000, address())), - (outpoint(3), tx_out(104, address())), - (outpoint(7), tx_out(102, address())), + (outpoint(4), tx_out(101, address(0))), + (outpoint(1), tx_out(20_000, address(0))), + (outpoint(2), tx_out(105, address(0))), + (outpoint(5), tx_out(103, address(0))), + (outpoint(6), tx_out(10_000, address(0))), + (outpoint(3), tx_out(104, address(0))), + (outpoint(7), tx_out(102, address(0))), ]; let mut tx_builder = TransactionBuilder::new( @@ -2058,7 +2058,7 @@ mod tests { #[test] fn build_transaction_with_custom_postage() { - let utxos = vec![(outpoint(1), tx_out(1_000_000, address()))]; + let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))]; let fee_rate = FeeRate::try_from(17.3).unwrap(); @@ -2096,7 +2096,7 @@ mod tests { #[test] fn select_cardinal_utxo_ignores_locked_utxos_and_errors_if_none_available() { - let utxos = vec![(outpoint(1), tx_out(500, address()))]; + let utxos = vec![(outpoint(1), tx_out(500, address(0)))]; let locked_utxos = vec![outpoint(1)]; let mut tx_builder = TransactionBuilder::new( @@ -2121,8 +2121,8 @@ mod tests { #[test] fn select_cardinal_utxo_ignores_locked_utxos() { let utxos = vec![ - (outpoint(1), tx_out(500, address())), - (outpoint(2), tx_out(500, address())), + (outpoint(1), tx_out(500, address(0))), + (outpoint(2), tx_out(500, address(0))), ]; let locked_utxos = vec![outpoint(1)]; @@ -2148,8 +2148,8 @@ mod tests { #[test] fn prefer_further_away_utxos_if_they_are_newly_under_target() { let utxos = vec![ - (outpoint(1), tx_out(510, address())), - (outpoint(2), tx_out(400, address())), + (outpoint(1), tx_out(510, address(0))), + (outpoint(2), tx_out(400, address(0))), ]; let mut tx_builder = TransactionBuilder::new( @@ -2174,8 +2174,8 @@ mod tests { #[test] fn prefer_further_away_utxos_if_they_are_newly_over_target() { let utxos = vec![ - (outpoint(1), tx_out(490, address())), - (outpoint(2), tx_out(600, address())), + (outpoint(1), tx_out(490, address(0))), + (outpoint(2), tx_out(600, address(0))), ]; let mut tx_builder = TransactionBuilder::new( diff --git a/tests/balances.rs b/tests/balances.rs index ca64ddfdae..0ade8992b0 100644 --- a/tests/balances.rs +++ b/tests/balances.rs @@ -1,4 +1,4 @@ -use {super::*, ord::subcommand::balances::Output}; +use super::*; #[test] fn flag_is_required() { @@ -17,11 +17,11 @@ fn no_runes() { let output = CommandBuilder::new("--regtest --index-runes balances") .core(&core) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); assert_eq!( output, - Output { + Balances { runes: BTreeMap::new() } ); @@ -40,11 +40,11 @@ fn with_runes() { let output = CommandBuilder::new("--regtest --index-runes balances") .core(&core) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); assert_eq!( output, - Output { + Balances { runes: [ ( SpacedRune::new(Rune(RUNE), 0), diff --git a/tests/lib.rs b/tests/lib.rs index e6f9647103..76b702c1c7 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -75,10 +75,12 @@ mod wallet; const RUNE: u128 = 99246114928149462; type Balance = ord::subcommand::wallet::balance::Output; +type Balances = ord::subcommand::balances::Output; type Batch = ord::wallet::batch::Output; type Create = ord::subcommand::wallet::create::Output; type Inscriptions = Vec; type Send = ord::subcommand::wallet::send::Output; +type Split = ord::subcommand::wallet::split::Output; type Supply = ord::subcommand::supply::Output; fn create_wallet(core: &mockcore::Handle, ord: &TestServer) { diff --git a/tests/wallet.rs b/tests/wallet.rs index 1a873ec79f..50c15c665a 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -24,4 +24,5 @@ mod sats; mod selection; mod send; mod sign; +mod split; mod transactions; diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs index 4b16e15204..8f8c9313d3 100644 --- a/tests/wallet/selection.rs +++ b/tests/wallet/selection.rs @@ -210,3 +210,65 @@ fn sending_rune_does_not_send_inscription() { .expected_stderr("error: not enough cardinal utxos\n") .run_and_extract_stdout(); } + +#[test] +fn split_does_not_select_inscribed_or_runic_utxos() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + let rune = Rune(RUNE); + + etch(&core, &ord, rune); + + etch(&core, &ord, Rune(RUNE + 1)); + + drain(&core, &ord); + + pretty_assert_eq!( + CommandBuilder::new("--regtest wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 0, + ordinal: 20000, + runic: Some(20000), + runes: Some( + [ + (SpacedRune { rune, spacers: 0 }, "1000".parse().unwrap()), + ( + SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0 + }, + "1000".parse().unwrap() + ), + ] + .into() + ), + total: 40000, + } + ); + + CommandBuilder::new("--regtest wallet split --fee-rate 0 --splits splits.yaml") + .core(&core) + .ord(&ord) + .write( + "splits.yaml", + format!( + " +outputs: +- address: bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + value: 20000 sat + runes: + {rune}: 1000 +" + ), + ) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} diff --git a/tests/wallet/split.rs b/tests/wallet/split.rs new file mode 100644 index 0000000000..63793fc8c9 --- /dev/null +++ b/tests/wallet/split.rs @@ -0,0 +1,293 @@ +use super::*; + +#[test] +fn requires_rune_index() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + CommandBuilder::new("wallet split --fee-rate 1 --splits splits.yaml") + .core(&core) + .ord(&ord) + .expected_stderr("error: `ord wallet split` requires index created with `--index-runes`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn unrecognized_fields_are_forbidden() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes"], &[]); + + create_wallet(&core, &ord); + + CommandBuilder::new("wallet split --fee-rate 1 --splits splits.yaml") + .core(&core) + .ord(&ord) + .write( + "splits.yaml", + " +foo: +outputs: +", + ) + .stderr_regex("error: unknown field `foo`.*") + .expected_exit_code(1) + .run_and_extract_stdout(); + + CommandBuilder::new("wallet split --fee-rate 1 --splits splits.yaml") + .core(&core) + .ord(&ord) + .write( + "splits.yaml", + " +outputs: +- address: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 + runes: + foo: +", + ) + .stderr_regex(r"error: outputs\[0\]: unknown field `foo`.*") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn cannot_split_un_etched_runes() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + let rune = Rune(RUNE); + + CommandBuilder::new("--regtest wallet split --fee-rate 1 --splits splits.yaml") + .core(&core) + .ord(&ord) + .write( + "splits.yaml", + format!( + " +outputs: +- address: bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + runes: + {rune}: 500 +" + ), + ) + .expected_stderr("error: rune `AAAAAAAAAAAAA` has not been etched\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn simple_split() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + let rune = Rune(RUNE); + let spaced_rune = SpacedRune { rune, spacers: 1 }; + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + supply: "100.0".parse().unwrap(), + divisibility: 1, + terms: None, + premine: "100.0".parse().unwrap(), + rune: SpacedRune { rune, spacers: 1 }, + symbol: '¢', + turbo: false, + }), + inscriptions: vec![batch::Entry { + file: Some("inscription.jpeg".into()), + ..default() + }], + ..default() + }, + ); + + pretty_assert_eq!( + CommandBuilder::new("--regtest wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 7 * 50 * COIN_VALUE - 20000, + ordinal: 10000, + runic: Some(10000), + runes: Some([(spaced_rune, "100.0".parse().unwrap())].into()), + total: 7 * 50 * COIN_VALUE, + } + ); + + let output = CommandBuilder::new( + "--regtest wallet split --fee-rate 10 --postage 666sat --splits splits.yaml", + ) + .core(&core) + .ord(&ord) + .write( + "splits.yaml", + format!( + " +outputs: +- address: bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + runes: + {spaced_rune}: 50.1 +" + ), + ) + .run_and_deserialize_output::(); + + assert_eq!(output.fee, 2030); + + core.mine_blocks_with_subsidy(1, 0); + + pretty_assert_eq!( + CommandBuilder::new("--regtest wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 7 * 50 * COIN_VALUE - 10960, + ordinal: 10000, + runic: Some(666), + runes: Some([(spaced_rune, "49.9".parse().unwrap())].into()), + total: 7 * 50 * COIN_VALUE - 294, + } + ); + + pretty_assert_eq!( + CommandBuilder::new("--regtest --index-runes balances") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balances { + runes: [( + spaced_rune, + [ + ( + OutPoint { + txid: output.txid, + vout: 1 + }, + Pile { + amount: 499, + divisibility: 1, + symbol: Some('¢'), + } + ), + ( + OutPoint { + txid: output.txid, + vout: 2 + }, + Pile { + amount: 501, + divisibility: 1, + symbol: Some('¢'), + } + ) + ] + .into() + ),] + .into(), + } + ); +} + +#[test] +fn oversize_op_returns_are_allowed_with_flag() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + let rune = Rune(RUNE); + + let spaced_rune = SpacedRune { rune, spacers: 1 }; + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + supply: "10000000000".parse().unwrap(), + divisibility: 0, + terms: None, + premine: "10000000000".parse().unwrap(), + rune: SpacedRune { rune, spacers: 1 }, + symbol: '¢', + turbo: false, + }), + inscriptions: vec![batch::Entry { + file: Some("inscription.jpeg".into()), + ..default() + }], + ..default() + }, + ); + + let mut splitfile = String::from("outputs:\n"); + + for _ in 0..10 { + splitfile.push_str( + "\n- address: bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + runes: + AAAAAAAAAAAAA: 1000000000", + ); + } + + CommandBuilder::new("--regtest wallet split --fee-rate 0 --splits splits.yaml") + .core(&core) + .ord(&ord) + .write("splits.yaml", &splitfile) + .expected_stderr("error: runestone size 85 over maximum standard OP_RETURN size 83\n") + .expected_exit_code(1) + .run_and_extract_stdout(); + + let output = + CommandBuilder::new("--regtest wallet split --fee-rate 0 --splits splits.yaml --no-limit") + .core(&core) + .ord(&ord) + .write("splits.yaml", &splitfile) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + pretty_assert_eq!( + CommandBuilder::new("--regtest --index-runes balances") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balances { + runes: [( + spaced_rune, + (0..10) + .map(|i| ( + OutPoint { + txid: output.txid, + vout: 1 + i, + }, + Pile { + amount: 1000000000, + divisibility: 0, + symbol: Some('¢'), + } + ),) + .collect() + )] + .into(), + } + ); +}