diff --git a/src/main.rs b/src/main.rs index 143d242994..3bb329aaab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ use { serde::{Deserialize, Serialize}, std::{ cmp::Ordering, - collections::VecDeque, + collections::{BTreeMap, VecDeque}, env, fmt::{self, Display, Formatter}, fs, io, diff --git a/src/sat_point.rs b/src/sat_point.rs index d40ffcde09..0d3e843b2e 100644 --- a/src/sat_point.rs +++ b/src/sat_point.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Copy, Clone)] pub(crate) struct SatPoint { pub(crate) outpoint: OutPoint, pub(crate) offset: u64, @@ -29,3 +29,53 @@ impl Decodable for SatPoint { }) } } + +impl FromStr for SatPoint { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (outpoint, offset) = s + .rsplit_once(':') + .ok_or_else(|| anyhow!("invalid satpoint: {s}"))?; + + Ok(SatPoint { + outpoint: outpoint.parse()?, + offset: offset.parse()?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str_ok() { + assert_eq!( + "1111111111111111111111111111111111111111111111111111111111111111:1:1" + .parse::() + .unwrap(), + SatPoint { + outpoint: "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse() + .unwrap(), + offset: 1, + } + ); + } + + #[test] + fn from_str_err() { + "abc".parse::().unwrap_err(); + + "abc:xyz".parse::().unwrap_err(); + + "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse::() + .unwrap_err(); + + "1111111111111111111111111111111111111111111111111111111111111111:1:foo" + .parse::() + .unwrap_err(); + } +} diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 5944485b64..1d9881904c 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -22,6 +22,23 @@ fn list_unspent(options: &Options, index: &Index) -> Result Result> { + let client = options.bitcoin_rpc_client()?; + + Ok( + client + .list_unspent(None, None, None, None, None)? + .iter() + .map(|utxo| { + let outpoint = OutPoint::new(utxo.txid, utxo.vout); + let amount = utxo.amount; + + (outpoint, amount) + }) + .collect(), + ) +} + fn get_change_addresses(options: &Options, n: usize) -> Result> { let client = options.bitcoin_rpc_client()?; diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 04d75e36e9..ce671ac45a 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -14,8 +14,8 @@ use { #[derive(Debug, Parser)] pub(crate) struct Inscribe { - #[clap(long, help = "Inscribe ")] - ordinal: Ordinal, + #[clap(long, help = "Inscribe ")] + satpoint: SatPoint, #[clap(long, help = "Inscribe ordinal with contents of ")] file: PathBuf, } @@ -36,7 +36,7 @@ impl Inscribe { let reveal_tx_destination = get_change_addresses(&options, 1)?[0].clone(); let (unsigned_commit_tx, reveal_tx) = Inscribe::create_inscription_transactions( - self.ordinal, + self.satpoint, inscription, options.chain.network(), utxos, @@ -62,7 +62,7 @@ impl Inscribe { } fn create_inscription_transactions( - ordinal: Ordinal, + satpoint: SatPoint, inscription: Inscription, network: bitcoin::Network, utxos: Vec<(OutPoint, Vec<(u64, u64)>)>, @@ -96,8 +96,16 @@ impl Inscribe { let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), network); let unsigned_commit_tx = TransactionBuilder::build_transaction( - utxos.into_iter().collect(), - ordinal, + satpoint, + utxos + .into_iter() + .map(|(outpoint, ranges)| { + ( + outpoint, + Amount::from_sat(ranges.iter().map(|(start, end)| end - start).sum()), + ) + }) + .collect(), commit_tx_address.clone(), change, )?; @@ -186,12 +194,11 @@ mod tests { fn reveal_transaction_pays_fee() { let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])]; let inscription = Inscription::Text("ord".into()); - let ordinal = Ordinal(10_000); let commit_address = change(0); let reveal_address = recipient(); let (commit_tx, reveal_tx) = Inscribe::create_inscription_transactions( - ordinal, + satpoint(1, 0), inscription, bitcoin::Network::Signet, utxos, @@ -211,13 +218,13 @@ mod tests { #[test] fn reveal_transaction_value_insufficient_to_pay_fee() { let utxos = vec![(outpoint(1), vec![(10_000, 11_000)])]; - let ordinal = Ordinal(10_000); + let satpoint = satpoint(1, 0); let inscription = Inscription::Png([1; 10_000].to_vec()); let commit_address = change(0); let reveal_address = recipient(); assert!(Inscribe::create_inscription_transactions( - ordinal, + satpoint, inscription, bitcoin::Network::Signet, utxos, @@ -233,12 +240,12 @@ mod tests { fn reveal_transaction_would_create_dust() { let utxos = vec![(outpoint(1), vec![(10_000, 10_600)])]; let inscription = Inscription::Text("ord".into()); - let ordinal = Ordinal(10_000); + let satpoint = satpoint(1, 0); let commit_address = change(0); let reveal_address = recipient(); let error = Inscribe::create_inscription_transactions( - ordinal, + satpoint, inscription, bitcoin::Network::Signet, utxos, diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 3f9550d276..90d4e07766 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Debug, Parser)] pub(crate) struct Send { - ordinal: Ordinal, + satpoint: SatPoint, address: Address, } @@ -18,15 +18,12 @@ impl Send { ); } - let index = Index::open(&options)?; - index.update()?; - - let utxos = list_unspent(&options, &index)?.into_iter().collect(); + let utxos = list_utxos(&options)?; let change = get_change_addresses(&options, 2)?; let unsigned_transaction = - TransactionBuilder::build_transaction(utxos, self.ordinal, self.address, change)?; + TransactionBuilder::build_transaction(self.satpoint, utxos, self.address, change)?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 2c3a050ebc..aa39432b8d 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -34,27 +34,18 @@ use { #[derive(Debug, PartialEq)] pub(crate) enum Error { - NotInWallet(Ordinal), + NotInWallet(SatPoint), NotEnoughCardinalUtxos, - RareOrdinalLostToRecipient(Ordinal), - RareOrdinalLostToFee(Ordinal), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::NotInWallet(ordinal) => write!(f, "ordinal {ordinal} not in wallet"), + Error::NotInWallet(satpoint) => write!(f, "satpoint {satpoint} not in wallet"), Error::NotEnoughCardinalUtxos => write!( f, "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." ), - Error::RareOrdinalLostToRecipient(ordinal) => write!( - f, - "transaction would lose rare ordinal {ordinal} to recipient" - ), - Error::RareOrdinalLostToFee(ordinal) => { - write!(f, "transaction would lose rare ordinal {ordinal} to fee") - } } } } @@ -63,13 +54,13 @@ impl std::error::Error for Error {} #[derive(Debug, PartialEq)] pub(crate) struct TransactionBuilder { + amounts: BTreeMap, change_addresses: BTreeSet
, - unused_change_addresses: Vec
, inputs: Vec, - ordinal: Ordinal, outputs: Vec<(Address, Amount)>, - ranges: BTreeMap>, recipient: Address, + satpoint: SatPoint, + unused_change_addresses: Vec
, utxos: BTreeSet, } @@ -81,12 +72,12 @@ impl TransactionBuilder { const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); pub(crate) fn build_transaction( - ranges: BTreeMap>, - ordinal: Ordinal, + satpoint: SatPoint, + amounts: BTreeMap, recipient: Address, change: Vec
, ) -> Result { - Self::new(ranges, ordinal, recipient, change) + Self::new(satpoint, amounts, recipient, change) .select_ordinal()? .align_ordinal() .pad_alignment_output()? @@ -97,40 +88,32 @@ impl TransactionBuilder { } fn new( - ranges: BTreeMap>, - ordinal: Ordinal, + satpoint: SatPoint, + amounts: BTreeMap, recipient: Address, change: Vec
, ) -> Self { Self { + utxos: amounts.keys().cloned().collect(), + amounts, change_addresses: change.iter().cloned().collect(), - utxos: ranges.keys().cloned().collect(), inputs: Vec::new(), - ordinal, outputs: Vec::new(), - ranges, recipient, + satpoint, unused_change_addresses: change, } } fn select_ordinal(mut self) -> Result { - let (ordinal_outpoint, ranges) = self - .ranges - .iter() - .find(|(_outpoint, ranges)| { - ranges - .iter() - .any(|(start, end)| self.ordinal.0 < *end && self.ordinal.0 >= *start) - }) - .map(|(outpoint, ranges)| (*outpoint, ranges.clone())) - .ok_or(Error::NotInWallet(self.ordinal))?; - - self.utxos.remove(&ordinal_outpoint); - self.inputs.push(ordinal_outpoint); + self.utxos.remove(&self.satpoint.outpoint); + self.inputs.push(self.satpoint.outpoint); self.outputs.push(( self.recipient.clone(), - Amount::from_sat(ranges.iter().map(|(start, end)| end - start).sum()), + *self + .amounts + .get(&self.satpoint.outpoint) + .ok_or(Error::NotInWallet(self.satpoint))?, )); Ok(self) @@ -283,7 +266,6 @@ impl TransactionBuilder { } fn build(self) -> Result { - let ordinal = self.ordinal.n(); let recipient = self.recipient.script_pubkey(); let transaction = Transaction { version: 1, @@ -308,21 +290,22 @@ impl TransactionBuilder { .collect(), }; - let outpoint = self - .ranges - .iter() - .find(|(_outpoint, ranges)| { - ranges - .iter() - .any(|(start, end)| ordinal >= *start && ordinal < *end) - }) - .expect("invariant: ordinal is contained in utxo ranges"); + assert_eq!( + self + .amounts + .iter() + .filter(|(outpoint, amount)| *outpoint == &self.satpoint.outpoint + && self.satpoint.offset < amount.to_sat()) + .count(), + 1, + "invariant: satpoint is contained in utxos" + ); assert_eq!( transaction .input .iter() - .filter(|tx_in| tx_in.previous_output == *outpoint.0) + .filter(|tx_in| tx_in.previous_output == self.satpoint.outpoint) .count(), 1, "invariant: inputs spend ordinal" @@ -330,20 +313,16 @@ impl TransactionBuilder { let mut ordinal_offset = 0; let mut found = false; - for (start, end) in transaction - .input - .iter() - .flat_map(|tx_in| &self.ranges[&tx_in.previous_output]) - { - if ordinal >= *start && ordinal < *end { - ordinal_offset += ordinal - start; + for tx_in in &transaction.input { + if tx_in.previous_output == self.satpoint.outpoint { + ordinal_offset += self.satpoint.offset; found = true; break; } else { - ordinal_offset += end - start; + ordinal_offset += self.amounts[&tx_in.previous_output].to_sat(); } } - assert!(found, "invariant: ordinal is found in inputs"); + assert!(found, "invariant: satpoint is found in inputs"); let mut output_end = 0; let mut found = false; @@ -409,12 +388,7 @@ impl TransactionBuilder { let mut fee = Amount::ZERO; for input in &transaction.input { - fee += Amount::from_sat( - self.ranges[&input.previous_output] - .iter() - .map(|(start, end)| end - start) - .sum::(), - ); + fee += self.amounts[&input.previous_output]; } for output in &transaction.output { fee -= Amount::from_sat(output.value); @@ -436,69 +410,27 @@ impl TransactionBuilder { ); } - let mut offset = 0; - let mut rare_ordinals = Vec::<(Ordinal, u64)>::new(); - for input in &transaction.input { - for (start, end) in &self.ranges[&input.previous_output] { - if Ordinal(*start).rarity() > Rarity::Common { - rare_ordinals.push((Ordinal(*start), offset)); - } - offset += end - start; - } - } - let total_input_amount = offset; - - let mut offset = 0; - let mut recipient_range = (0, 0); - for output in &transaction.output { - if output.script_pubkey == self.recipient.script_pubkey() { - recipient_range = (offset, offset + output.value); - break; - } - offset += output.value; - } - - for (rare_ordinal, offset) in &rare_ordinals { - if rare_ordinal != &self.ordinal { - if offset >= &recipient_range.0 && offset < &recipient_range.1 { - return Err(Error::RareOrdinalLostToRecipient(*rare_ordinal)); - } else if offset >= &(total_input_amount - fee.to_sat()) { - return Err(Error::RareOrdinalLostToFee(*rare_ordinal)); - } - } - } - Ok(transaction) } fn calculate_ordinal_offset(&self) -> u64 { let mut ordinal_offset = 0; - for (start, end) in self.inputs.iter().flat_map(|input| &self.ranges[input]) { - if self.ordinal.0 >= *start && self.ordinal.0 < *end { - ordinal_offset += self.ordinal.0 - start; - return ordinal_offset; + for outpoint in &self.inputs { + if *outpoint == self.satpoint.outpoint { + return ordinal_offset + self.satpoint.offset; } else { - ordinal_offset += end - start; + ordinal_offset += self.amounts[outpoint].to_sat(); } } - panic!("Could not find ordinal in inputs"); + + panic!("Could not find satpoint in inputs"); } fn select_cardinal_utxo(&mut self, minimum_amount: Amount) -> Result<(OutPoint, Amount)> { let mut found = None; for utxo in &self.utxos { - if self.ranges[utxo] - .iter() - .any(|(start, _end)| Ordinal(*start).rarity() > Rarity::Common) - { - continue; - } - - let amount = self.ranges[utxo] - .iter() - .map(|(start, end)| Amount::from_sat(end - start)) - .sum::(); + let amount = self.amounts[utxo]; if amount >= minimum_amount { found = Some((*utxo, amount)); @@ -521,14 +453,14 @@ mod tests { #[test] fn select_ordinal() { let mut utxos = vec![ - (outpoint(1), vec![(10_000, 15_000)]), - (outpoint(2), vec![(51 * COIN_VALUE, 100 * COIN_VALUE)]), - (outpoint(3), vec![(6_000, 8_000)]), + (outpoint(1), Amount::from_sat(5_000)), + (outpoint(2), Amount::from_sat(49 * COIN_VALUE)), + (outpoint(3), Amount::from_sat(2_000)), ]; let tx_builder = TransactionBuilder::new( + satpoint(2, 0), utxos.clone().into_iter().collect(), - Ordinal(51 * COIN_VALUE), recipient(), vec![change(0), change(1)], ) @@ -552,15 +484,15 @@ mod tests { #[test] fn tx_builder_to_transaction() { - let mut ranges = BTreeMap::new(); - ranges.insert(outpoint(1), vec![(0, 5_000)]); - ranges.insert(outpoint(2), vec![(10_000, 15_000)]); - ranges.insert(outpoint(3), vec![(6_000, 8_000)]); + let mut amounts = BTreeMap::new(); + amounts.insert(outpoint(1), Amount::from_sat(5_000)); + amounts.insert(outpoint(2), Amount::from_sat(5_000)); + amounts.insert(outpoint(3), Amount::from_sat(2_000)); let tx_builder = TransactionBuilder { - ranges, + amounts, utxos: BTreeSet::new(), - ordinal: Ordinal(0), + satpoint: satpoint(1, 0), recipient: recipient(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), @@ -589,12 +521,12 @@ mod tests { #[test] fn deduct_fee() { - let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 0), utxos.into_iter().collect(), - Ordinal(10_000), recipient(), vec![change(0), change(1)], ), @@ -610,11 +542,11 @@ mod tests { #[test] #[should_panic(expected = "invariant: deducting fee does not consume ordinal")] fn invariant_deduct_fee_does_not_consume_ordinal() { - let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; TransactionBuilder::new( + satpoint(1, 4_950), utxos.into_iter().collect(), - Ordinal(14_950), recipient(), vec![change(0), change(1)], ) @@ -628,14 +560,14 @@ mod tests { #[test] fn additional_postage_added_when_required() { let utxos = vec![ - (outpoint(1), vec![(10_000, 15_000)]), - (outpoint(2), vec![(5_000, 10_000)]), + (outpoint(1), Amount::from_sat(5_000)), + (outpoint(2), Amount::from_sat(5_000)), ]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 4_950), utxos.into_iter().collect(), - Ordinal(14_950), recipient(), vec![change(0), change(1)], ), @@ -650,12 +582,12 @@ mod tests { #[test] fn insufficient_padding_to_add_postage_no_utxos() { - let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 4_950), utxos.into_iter().collect(), - Ordinal(14_950), recipient(), vec![change(0), change(1)], ), @@ -666,14 +598,14 @@ mod tests { #[test] fn insufficient_padding_to_add_postage_small_utxos() { let utxos = vec![ - (outpoint(1), vec![(10_000, 15_000)]), - (outpoint(2), vec![(0, 1)]), + (outpoint(1), Amount::from_sat(5_000)), + (outpoint(2), Amount::from_sat(1)), ]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 4_950), utxos.into_iter().collect(), - Ordinal(14_950), recipient(), vec![change(0), change(1)], ), @@ -684,14 +616,14 @@ mod tests { #[test] fn excess_additional_postage_is_stripped() { let utxos = vec![ - (outpoint(1), vec![(10_000, 15_000)]), - (outpoint(2), vec![(15_000, 35_000)]), + (outpoint(1), Amount::from_sat(5_000)), + (outpoint(2), Amount::from_sat(20_000)), ]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 4_950), utxos.into_iter().collect(), - Ordinal(14_950), recipient(), vec![change(0), change(1)], ), @@ -709,11 +641,28 @@ mod tests { } #[test] - #[should_panic(expected = "invariant: ordinal is contained in utxo ranges")] - fn invariant_ordinal_is_contained_in_utxo_ranges() { + #[should_panic(expected = "invariant: satpoint is contained in utxos")] + fn invariant_satpoint_outpoint_is_contained_in_utxos() { TransactionBuilder::new( - [(outpoint(1), vec![(0, 2), (3, 5)])].into_iter().collect(), - Ordinal(2), + satpoint(2, 0), + vec![(outpoint(1), Amount::from_sat(4))] + .into_iter() + .collect(), + recipient(), + vec![change(0), change(1)], + ) + .build() + .unwrap(); + } + + #[test] + #[should_panic(expected = "invariant: satpoint is contained in utxos")] + fn invariant_satpoint_offset_is_contained_in_utxos() { + TransactionBuilder::new( + satpoint(1, 4), + vec![(outpoint(1), Amount::from_sat(4))] + .into_iter() + .collect(), recipient(), vec![change(0), change(1)], ) @@ -725,8 +674,10 @@ mod tests { #[should_panic(expected = "invariant: inputs spend ordinal")] fn invariant_inputs_spend_ordinal() { TransactionBuilder::new( - [(outpoint(1), vec![(0, 5)])].into_iter().collect(), - Ordinal(2), + satpoint(1, 2), + vec![(outpoint(1), Amount::from_sat(5))] + .into_iter() + .collect(), recipient(), vec![change(0), change(1)], ) @@ -738,8 +689,10 @@ mod tests { #[should_panic(expected = "invariant: ordinal is sent to recipient")] fn invariant_ordinal_is_sent_to_recipient() { let mut builder = TransactionBuilder::new( - [(outpoint(1), vec![(0, 5)])].into_iter().collect(), - Ordinal(2), + satpoint(1, 2), + vec![(outpoint(1), Amount::from_sat(5))] + .into_iter() + .collect(), recipient(), vec![change(0), change(1)], ) @@ -757,8 +710,10 @@ mod tests { #[should_panic(expected = "invariant: ordinal is found in outputs")] fn invariant_ordinal_is_found_in_outputs() { let mut builder = TransactionBuilder::new( - [(outpoint(1), vec![(0, 5)])].into_iter().collect(), - Ordinal(2), + satpoint(1, 2), + vec![(outpoint(1), Amount::from_sat(5))] + .into_iter() + .collect(), recipient(), vec![change(0), change(1)], ) @@ -772,12 +727,12 @@ mod tests { #[test] fn excess_postage_is_stripped() { - let utxos = vec![(outpoint(1), vec![(0, 1_000_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(1_000_000))]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 0), utxos.into_iter().collect(), - Ordinal(0), recipient(), vec![change(0), change(1)] ), @@ -796,11 +751,11 @@ mod tests { #[test] #[should_panic(expected = "invariant: excess postage is stripped")] fn invariant_excess_postage_is_stripped() { - let utxos = vec![(outpoint(1), vec![(0, 1_000_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(1_000_000))]; TransactionBuilder::new( + satpoint(1, 0), utxos.into_iter().collect(), - Ordinal(0), recipient(), vec![change(0), change(1)], ) @@ -812,12 +767,12 @@ mod tests { #[test] fn ordinal_is_aligned() { - let utxos = vec![(outpoint(1), vec![(0, 10_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 3_333), utxos.into_iter().collect(), - Ordinal(3_333), recipient(), vec![change(0), change(1)] ), @@ -833,14 +788,14 @@ mod tests { #[test] fn alignment_output_under_dust_limit_is_padded() { let utxos = vec![ - (outpoint(1), vec![(0, 10_000)]), - (outpoint(2), vec![(10_000, 20_000)]), + (outpoint(1), Amount::from_sat(10_000)), + (outpoint(2), Amount::from_sat(10_000)), ]; pretty_assert_eq!( TransactionBuilder::build_transaction( + satpoint(1, 1), utxos.into_iter().collect(), - Ordinal(1), recipient(), vec![change(0), change(1)] ), @@ -856,11 +811,11 @@ 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), vec![(0, 10_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; let mut builder = TransactionBuilder::new( + satpoint(1, 3_333), utxos.into_iter().collect(), - Ordinal(3_333), recipient(), vec![change(0), change(1)], ) @@ -880,11 +835,11 @@ 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), vec![(0, 10_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; TransactionBuilder::new( + satpoint(1, 1), utxos.into_iter().collect(), - Ordinal(1), recipient(), vec![change(0), change(1)], ) @@ -902,11 +857,11 @@ mod tests { #[test] #[should_panic(expected = "invariant: ordinal is at first position in recipient output")] fn invariant_ordinal_is_aligned() { - let utxos = vec![(outpoint(1), vec![(0, 10_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; TransactionBuilder::new( + satpoint(1, 3_333), utxos.into_iter().collect(), - Ordinal(3_333), recipient(), vec![change(0), change(1)], ) @@ -921,11 +876,11 @@ mod tests { #[test] #[should_panic(expected = "invariant: fee rate is equal to target fee rate")] fn invariant_fee_is_at_least_target_fee_rate() { - let utxos = vec![(outpoint(1), vec![(0, 10_000)])]; + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; TransactionBuilder::new( + satpoint(1, 0), utxos.into_iter().collect(), - Ordinal(0), recipient(), vec![change(0), change(1)], ) @@ -936,42 +891,18 @@ mod tests { .unwrap(); } - #[test] - fn rare_ordinals_are_not_used_as_cardinal_inputs() { - let utxos = vec![ - (outpoint(1), vec![(10_000, 15_000)]), - (outpoint(2), vec![(0, 5_000)]), - (outpoint(3), vec![(5_000, 10_000)]), - ]; - - pretty_assert_eq!( - TransactionBuilder::build_transaction( - utxos.into_iter().collect(), - Ordinal(14_950), - recipient(), - vec![change(0), change(1),], - ), - Ok(Transaction { - version: 1, - lock_time: PackedLockTime::ZERO, - input: vec![tx_in(outpoint(1)), tx_in(outpoint(3))], - output: vec![tx_out(4_950, change(1)), tx_out(4_620, recipient())], - }) - ) - } - #[test] #[should_panic(expected = "invariant: recipient address appears exactly once in outputs")] fn invariant_recipient_appears_exactly_once() { - let mut ranges = BTreeMap::new(); - ranges.insert(outpoint(1), vec![(0, 5_000)]); - ranges.insert(outpoint(2), vec![(10_000, 15_000)]); - ranges.insert(outpoint(3), vec![(6_000, 8_000)]); + let mut amounts = BTreeMap::new(); + amounts.insert(outpoint(1), Amount::from_sat(5_000)); + amounts.insert(outpoint(2), Amount::from_sat(5_000)); + amounts.insert(outpoint(3), Amount::from_sat(2_000)); TransactionBuilder { - ranges, + amounts, utxos: BTreeSet::new(), - ordinal: Ordinal(0), + satpoint: satpoint(1, 0), recipient: recipient(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), @@ -989,15 +920,15 @@ mod tests { #[test] #[should_panic(expected = "invariant: change addresses appear at most once in outputs")] fn invariant_change_appears_at_most_once() { - let mut ranges = BTreeMap::new(); - ranges.insert(outpoint(1), vec![(0, 5_000)]); - ranges.insert(outpoint(2), vec![(10_000, 15_000)]); - ranges.insert(outpoint(3), vec![(6_000, 8_000)]); + let mut amounts = BTreeMap::new(); + amounts.insert(outpoint(1), Amount::from_sat(5_000)); + amounts.insert(outpoint(2), Amount::from_sat(5_000)); + amounts.insert(outpoint(3), Amount::from_sat(2_000)); TransactionBuilder { - ranges, + amounts, utxos: BTreeSet::new(), - ordinal: Ordinal(0), + satpoint: satpoint(1, 0), recipient: recipient(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), @@ -1011,34 +942,4 @@ mod tests { .build() .unwrap(); } - - #[test] - fn rare_ordinals_are_not_sent_to_recipient() { - let utxos = vec![(outpoint(1), vec![(15_000, 25_000), (0, 10_000)])]; - - pretty_assert_eq!( - TransactionBuilder::build_transaction( - utxos.into_iter().collect(), - Ordinal(24_000), - recipient(), - vec![change(0), change(1),], - ), - Err(Error::RareOrdinalLostToRecipient(Ordinal(0))) - ) - } - - #[test] - fn rare_ordinals_are_not_sent_as_fee() { - let utxos = vec![(outpoint(1), vec![(15_000, 25_000), (0, 100)])]; - - pretty_assert_eq!( - TransactionBuilder::build_transaction( - utxos.into_iter().collect(), - Ordinal(24_000), - recipient(), - vec![change(0), change(1),], - ), - Err(Error::RareOrdinalLostToFee(Ordinal(0))) - ) - } } diff --git a/src/test.rs b/src/test.rs index 89a1189819..2f9a412986 100644 --- a/src/test.rs +++ b/src/test.rs @@ -27,6 +27,13 @@ pub(crate) fn outpoint(n: u64) -> OutPoint { format!("{}:{}", hex.repeat(64), n).parse().unwrap() } +pub(crate) fn satpoint(n: u64, offset: u64) -> SatPoint { + SatPoint { + outpoint: outpoint(n), + offset, + } +} + pub(crate) fn recipient() -> Address { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() diff --git a/tests/server.rs b/tests/server.rs index 660a9a68af..a82a4f8096 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -37,11 +37,11 @@ fn run() { #[test] fn inscription_page() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - let stdout = CommandBuilder::new( - "--chain regtest --index-ordinals wallet inscribe --ordinal 5000000000 --file hello.txt", - ) + let stdout = CommandBuilder::new(format!( + "--chain regtest --index-ordinals wallet inscribe --satpoint {txid}:0:0 --file hello.txt" + )) .write("hello.txt", "HELLOWORLD") .rpc_server(&rpc_server) .stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n") diff --git a/tests/wallet.rs b/tests/wallet.rs index 6e397ef507..cf6e315a8b 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -59,10 +59,10 @@ fn identify_from_tsv_file_not_found() { fn send_works_on_signet() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Signet, "ord"); - rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); let stdout = CommandBuilder::new( - "--chain signet --index-ordinals wallet send 5000000000 tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw", + format!("--chain signet --index-ordinals wallet send {txid}:0:0 tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw") ) .rpc_server(&rpc_server) .stdout_regex(r".*") @@ -75,10 +75,10 @@ fn send_works_on_signet() { #[test] fn send_on_mainnnet_refuses_to_work_with_wallet_name_foo() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "foo"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new( - "--index-ordinals wallet send 5000000000 bc1qzjeg3h996kw24zrg69nge97fw8jc4v7v7yznftzk06j3429t52vse9tkp9", + format!("--index-ordinals wallet send {txid}:0:0 bc1qzjeg3h996kw24zrg69nge97fw8jc4v7v7yznftzk06j3429t52vse9tkp9"), ) .rpc_server(&rpc_server) .expected_stderr("error: `ord wallet send` may only be used on mainnet with a wallet named `ord` or whose name starts with `ord-`\n") @@ -89,24 +89,26 @@ fn send_on_mainnnet_refuses_to_work_with_wallet_name_foo() { #[test] fn send_addresses_must_be_valid_for_network() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "ord"); - rpc_server.mine_blocks_with_subsidy(1, 1_000_000); + let txid = rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0].txid(); - CommandBuilder::new("wallet send 5000000000 tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw") - .rpc_server(&rpc_server) - .expected_stderr( - "error: Address `tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw` is not valid for mainnet\n", - ) - .expected_exit_code(1) - .run(); + CommandBuilder::new(format!( + "wallet send {txid}:0:0 tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" + )) + .rpc_server(&rpc_server) + .expected_stderr( + "error: Address `tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw` is not valid for mainnet\n", + ) + .expected_exit_code(1) + .run(); } #[test] fn send_on_mainnnet_works_with_wallet_named_ord() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "ord"); - rpc_server.mine_blocks_with_subsidy(1, 1_000_000); + let txid = rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0].txid(); let stdout = CommandBuilder::new( - "--index-ordinals wallet send 5000000000 bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + format!("--index-ordinals wallet send {txid}:0:0 bc1qzjeg3h996kw24zrg69nge97fw8jc4v7v7yznftzk06j3429t52vse9tkp9"), ) .rpc_server(&rpc_server) .stdout_regex(r".*") @@ -119,11 +121,11 @@ fn send_on_mainnnet_works_with_wallet_named_ord() { #[test] fn send_on_mainnnet_works_with_wallet_whose_name_starts_with_ord() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "ord-foo"); - rpc_server.mine_blocks_with_subsidy(1, 1_000_000); + let txid = rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0].txid(); - let stdout = CommandBuilder::new( - "--index-ordinals wallet send 5000000000 bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", - ) + let stdout = CommandBuilder::new(format!( + "--index-ordinals wallet send {txid}:0:0 bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" + )) .rpc_server(&rpc_server) .stdout_regex(r".*") .run(); @@ -135,9 +137,9 @@ fn send_on_mainnnet_works_with_wallet_whose_name_starts_with_ord() { #[test] fn send_on_mainnnet_refuses_to_work_with_wallet_with_high_balance() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "ord"); - rpc_server.mine_blocks_with_subsidy(1, 1_000_001); + let txid = rpc_server.mine_blocks_with_subsidy(1, 1_000_001)[0].txdata[0].txid(); - CommandBuilder::new("wallet send 5000000000 bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh") + CommandBuilder::new(format!("wallet send {txid}:0:0 bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")) .rpc_server(&rpc_server) .expected_stderr( "error: `ord wallet send` may not be used on mainnet with wallets containing more than 1,000,000 sats\n", @@ -149,11 +151,11 @@ fn send_on_mainnnet_refuses_to_work_with_wallet_with_high_balance() { #[test] fn inscribe() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - CommandBuilder::new( - "--chain regtest --index-ordinals wallet inscribe --ordinal 5000000000 --file hello.txt", - ) + CommandBuilder::new(format!( + "--chain regtest --index-ordinals wallet inscribe --satpoint {txid}:0:0 --file hello.txt" + )) .write("hello.txt", "HELLOWORLD") .rpc_server(&rpc_server) .stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n") @@ -172,36 +174,40 @@ fn inscribe() { #[test] fn inscribe_forbidden_on_mainnet() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Bitcoin, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - CommandBuilder::new("wallet inscribe --ordinal 5000000000 --file hello.txt") - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr("error: `ord wallet inscribe` is unstable and not yet supported on mainnet.\n") - .run(); + CommandBuilder::new(format!( + "wallet inscribe --satpoint {txid}:0:0 --file hello.txt" + )) + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: `ord wallet inscribe` is unstable and not yet supported on mainnet.\n") + .run(); } #[test] fn inscribe_unknown_file_extension() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - CommandBuilder::new("--chain regtest wallet inscribe --ordinal 5000000000 --file pepe.jpg") - .write("pepe.jpg", [1; 520]) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr("error: unrecognized file extension `.jpg`, only .txt and .png accepted\n") - .run(); + CommandBuilder::new(format!( + "--chain regtest wallet inscribe --satpoint {txid}:0:0 --file pepe.jpg" + )) + .write("pepe.jpg", [1; 520]) + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: unrecognized file extension `.jpg`, only .txt and .png accepted\n") + .run(); } #[test] fn inscribe_png() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - CommandBuilder::new( - "--chain regtest --index-ordinals wallet inscribe --ordinal 5000000000 --file degenerate.png", - ) + CommandBuilder::new(format!( + "--chain regtest --index-ordinals wallet inscribe --satpoint {txid}:0:0 --file degenerate.png" + )) .write("degenerate.png", [1; 520]) .rpc_server(&rpc_server) .stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n") @@ -220,12 +226,14 @@ fn inscribe_png() { #[test] fn inscribe_exceeds_push_byte_limit() { let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord"); - rpc_server.mine_blocks(1); + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); - CommandBuilder::new("--chain regtest wallet inscribe --ordinal 5000000000 --file degenerate.png") - .write("degenerate.png", [1; 521]) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr("error: file size exceeds 520 bytes\n") - .run(); + CommandBuilder::new(format!( + "--chain regtest wallet inscribe --satpoint {txid}:0:0 --file degenerate.png" + )) + .write("degenerate.png", [1; 521]) + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: file size exceeds 520 bytes\n") + .run(); }