From 9ec5d4b6b4ef8196dbe47cb218a95e325b549737 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 28 Mar 2024 17:31:59 -0700 Subject: [PATCH] Add `ord wallet batch` command (#3401) --- docs/src/guides/batch-inscribing.md | 2 +- src/subcommand/wallet.rs | 6 + src/subcommand/wallet/batch_command.rs | 258 +++ src/subcommand/wallet/inscribe.rs | 1542 +------------ src/subcommand/wallet/shared_args.rs | 24 + src/wallet/batch.rs | 1118 ++++++++++ tests/lib.rs | 20 +- tests/server.rs | 2 +- tests/wallet.rs | 1 + tests/wallet/batch_command.rs | 2427 ++++++++++++++++++++ tests/wallet/inscribe.rs | 2792 ++---------------------- tests/wallet/send.rs | 2 +- 12 files changed, 4072 insertions(+), 4122 deletions(-) create mode 100644 src/subcommand/wallet/batch_command.rs create mode 100644 src/subcommand/wallet/shared_args.rs create mode 100644 tests/wallet/batch_command.rs diff --git a/docs/src/guides/batch-inscribing.md b/docs/src/guides/batch-inscribing.md index a20706200b..9ccb539cb0 100644 --- a/docs/src/guides/batch-inscribing.md +++ b/docs/src/guides/batch-inscribing.md @@ -11,7 +11,7 @@ To create a batch inscription using a batchfile in `batch.yaml`, run the following command: ```bash -ord wallet inscribe --fee-rate 21 --batch batch.yaml +ord wallet batch --fee-rate 21 --batch batch.yaml ``` Example `batch.yaml` diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 9c4e115dcf..8c434a2e81 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -3,9 +3,11 @@ use { crate::wallet::{batch, Wallet}, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, reqwest::Url, + shared_args::SharedArgs, }; pub mod balance; +mod batch_command; pub mod cardinals; pub mod create; pub mod dump; @@ -17,6 +19,7 @@ pub mod receive; pub mod restore; pub mod sats; pub mod send; +mod shared_args; pub mod transactions; #[derive(Debug, Parser)] @@ -39,6 +42,8 @@ pub(crate) struct WalletCommand { pub(crate) enum Subcommand { #[command(about = "Get wallet balance")] Balance, + #[command(about = "Create inscriptions and runes")] + Batch(batch_command::Batch), #[command(about = "Create new wallet")] Create(create::Create), #[command(about = "Dump wallet descriptors")] @@ -89,6 +94,7 @@ impl WalletCommand { match self.subcommand { Subcommand::Balance => balance::run(wallet), + Subcommand::Batch(batch) => batch.run(wallet), Subcommand::Dump => dump::run(wallet), Subcommand::Inscribe(inscribe) => inscribe.run(wallet), Subcommand::Inscriptions => inscriptions::run(wallet), diff --git a/src/subcommand/wallet/batch_command.rs b/src/subcommand/wallet/batch_command.rs new file mode 100644 index 0000000000..21fd3fe829 --- /dev/null +++ b/src/subcommand/wallet/batch_command.rs @@ -0,0 +1,258 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(crate) struct Batch { + #[command(flatten)] + shared: SharedArgs, + #[arg( + long, + help = "Inscribe multiple inscriptions and rune defined in YAML ." + )] + pub(crate) batch: PathBuf, +} + +impl Batch { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + let utxos = wallet.utxos(); + + let batchfile = batch::File::load(&self.batch)?; + + let parent_info = wallet.get_parent_info(batchfile.parent)?; + + let (inscriptions, reveal_satpoints, postages, destinations) = batchfile.inscriptions( + &wallet, + utxos, + parent_info.as_ref().map(|info| info.tx_out.value), + self.shared.compress, + )?; + + let mut locked_utxos = wallet.locked_utxos().clone(); + + locked_utxos.extend( + reveal_satpoints + .iter() + .map(|(satpoint, txout)| (satpoint.outpoint, txout.clone())), + ); + + if let Some(etching) = batchfile.etching { + Self::check_etching(&wallet, &etching)?; + } + + batch::Plan { + commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate), + destinations, + dry_run: self.shared.dry_run, + etching: batchfile.etching, + inscriptions, + mode: batchfile.mode, + no_backup: self.shared.no_backup, + no_limit: self.shared.no_limit, + parent_info, + postages, + reinscribe: batchfile.reinscribe, + reveal_fee_rate: self.shared.fee_rate, + reveal_satpoints, + satpoint: if let Some(sat) = batchfile.sat { + Some(wallet.find_sat_in_outputs(sat)?) + } else { + batchfile.satpoint + }, + } + .inscribe( + &locked_utxos.into_keys().collect(), + wallet.get_runic_outputs()?, + utxos, + &wallet, + ) + } + + fn check_etching(wallet: &Wallet, etching: &batch::Etching) -> Result { + let rune = etching.rune.rune; + + ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); + + ensure!( + etching.divisibility <= Etching::MAX_DIVISIBILITY, + " must be less than or equal 38" + ); + + ensure!( + wallet.has_rune_index(), + "etching runes requires index created with `--index-runes`", + ); + + ensure!( + wallet.get_rune(rune)?.is_none(), + "rune `{rune}` has already been etched", + ); + + let premine = etching.premine.to_integer(etching.divisibility)?; + + let supply = etching.supply.to_integer(etching.divisibility)?; + + let mintable = etching + .terms + .map(|terms| -> Result { + terms + .cap + .checked_mul(terms.amount.to_integer(etching.divisibility)?) + .ok_or_else(|| anyhow!("`terms.count` * `terms.amount` over maximum")) + }) + .transpose()? + .unwrap_or_default(); + + ensure!( + supply + == premine + .checked_add(mintable) + .ok_or_else(|| anyhow!("`premine` + `terms.count` * `terms.amount` over maximum"))?, + "`supply` not equal to `premine` + `terms.count` * `terms.amount`" + ); + + ensure!(supply > 0, "`supply` must be greater than zero"); + + let bitcoin_client = wallet.bitcoin_client(); + + let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap(); + + let reveal_height = current_height + 1 + RUNE_COMMIT_INTERVAL; + + if let Some(terms) = etching.terms { + if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.offset.end` must be greater than `terms.offset.start`" + ); + } + + if let Some((start, end)) = terms.height.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.height.end` must be greater than `terms.height.start`" + ); + } + + if let Some(end) = terms.height.and_then(|range| range.end) { + ensure!( + end > reveal_height.into(), + "`terms.height.end` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + if let Some(start) = terms.height.and_then(|range| range.start) { + ensure!( + start > reveal_height.into(), + "`terms.height.start` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + ensure!(terms.cap > 0, "`terms.cap` must be greater than zero"); + + ensure!( + terms.amount.to_integer(etching.divisibility)? > 0, + "`terms.amount` must be greater than zero", + ); + } + + let minimum = Rune::minimum_at_height(wallet.chain().into(), Height(reveal_height)); + + ensure!( + rune >= minimum, + "rune is less than minimum for next block: {rune} < {minimum}", + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::wallet::batch, + serde_yaml::{Mapping, Value}, + tempfile::TempDir, + }; + + #[test] + fn batch_is_loaded_from_yaml_file() { + let parent = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" + .parse::() + .unwrap(); + + let tempdir = TempDir::new().unwrap(); + + let inscription_path = tempdir.path().join("tulip.txt"); + fs::write(&inscription_path, "tulips are pretty").unwrap(); + + let brc20_path = tempdir.path().join("token.json"); + + let batch_path = tempdir.path().join("batch.yaml"); + fs::write( + &batch_path, + format!( + "mode: separate-outputs +parent: {parent} +inscriptions: +- file: {} + metadata: + title: Lorem Ipsum + description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante. +- file: {} + metaprotocol: brc-20 +", + inscription_path.display(), + brc20_path.display() + ), + ) + .unwrap(); + + let mut metadata = Mapping::new(); + metadata.insert( + Value::String("title".to_string()), + Value::String("Lorem Ipsum".to_string()), + ); + metadata.insert(Value::String("description".to_string()), Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.".to_string())); + + assert_eq!( + batch::File::load(&batch_path).unwrap(), + batch::File { + inscriptions: vec![ + batch::Entry { + file: inscription_path, + metadata: Some(Value::Mapping(metadata)), + ..default() + }, + batch::Entry { + file: brc20_path, + metaprotocol: Some("brc-20".to_string()), + ..default() + } + ], + parent: Some(parent), + ..default() + } + ); + } + + #[test] + fn batch_with_unknown_field_throws_error() { + let tempdir = TempDir::new().unwrap(); + let batch_path = tempdir.path().join("batch.yaml"); + fs::write( + &batch_path, + "mode: shared-output\ninscriptions:\n- file: meow.wav\nunknown: 1.)what", + ) + .unwrap(); + + assert!(batch::File::load(&batch_path) + .unwrap_err() + .to_string() + .contains("unknown field `unknown`")); + } + + #[test] + fn example_batchfile_deserializes_successfully() { + batch::File::load(Path::new("batch.yaml")).unwrap(); + } +} diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index d5975277ac..25ae0e51b5 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,44 +1,21 @@ use super::*; #[derive(Debug, Parser)] -#[clap( - group = ArgGroup::new("source") - .required(true) - .args(&["file", "batch"]), -)] pub(crate) struct Inscribe { - #[arg( - long, - help = "Inscribe multiple inscriptions defined in a yaml .", - conflicts_with_all = &[ - "cbor_metadata", "delegate", "destination", "file", "json_metadata", "metaprotocol", - "parent", "postage", "reinscribe", "sat", "satpoint" - ] - )] - pub(crate) batch: Option, + #[command(flatten)] + shared: SharedArgs, #[arg( long, help = "Include CBOR in file at as inscription metadata", conflicts_with = "json_metadata" )] pub(crate) cbor_metadata: Option, - #[arg( - long, - help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." - )] - pub(crate) commit_fee_rate: Option, - #[arg(long, help = "Compress inscription content with brotli.")] - pub(crate) compress: bool, #[arg(long, help = "Delegate inscription content to .")] pub(crate) delegate: Option, #[arg(long, help = "Send inscription to .")] pub(crate) destination: Option>, - #[arg(long, help = "Don't sign or broadcast transactions.")] - pub(crate) dry_run: bool, - #[arg(long, help = "Use fee rate of sats/vB.")] - pub(crate) fee_rate: FeeRate, #[arg(long, help = "Inscribe sat with contents of .")] - pub(crate) file: Option, + pub(crate) file: PathBuf, #[arg( long, help = "Include JSON in file at converted to CBOR as inscription metadata", @@ -47,14 +24,6 @@ pub(crate) struct Inscribe { pub(crate) json_metadata: Option, #[clap(long, help = "Set inscription metaprotocol to .")] pub(crate) metaprotocol: Option, - #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")] - pub(crate) no_backup: bool, - #[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." - )] - pub(crate) no_limit: bool, #[clap(long, help = "Make inscription a child of .")] pub(crate) parent: Option, #[arg( @@ -72,126 +41,52 @@ pub(crate) struct Inscribe { impl Inscribe { pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - let metadata = Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?; - - let utxos = wallet.utxos(); - - let mut locked_utxos = wallet.locked_utxos().clone(); - - let runic_utxos = wallet.get_runic_outputs()?; - let chain = wallet.chain(); - let destinations; - let inscriptions; - let mode; - let parent_info; - let postages; - let reinscribe; - let reveal_satpoints; - let etching; - - let satpoint = match (self.file, self.batch) { - (Some(file), None) => { - parent_info = wallet.get_parent_info(self.parent)?; - - postages = vec![self.postage.unwrap_or(TARGET_POSTAGE)]; - - if let Some(delegate) = self.delegate { - ensure! { - wallet.inscription_exists(delegate)?, - "delegate {delegate} does not exist" - } - } - - inscriptions = vec![Inscription::from_file( - chain, - self.compress, - self.delegate, - metadata, - self.metaprotocol, - self.parent.into_iter().collect(), - file, - None, - None, - )?]; - - mode = batch::Mode::SeparateOutputs; - - reinscribe = self.reinscribe; - - reveal_satpoints = Vec::new(); - - destinations = vec![match self.destination.clone() { - Some(destination) => destination.require_network(chain.network())?, - None => wallet.get_change_address()?, - }]; - - etching = None; - - if let Some(sat) = self.sat { - Some(wallet.find_sat_in_outputs(sat)?) - } else { - self.satpoint - } + if let Some(delegate) = self.delegate { + ensure! { + wallet.inscription_exists(delegate)?, + "delegate {delegate} does not exist" } - (None, Some(batch)) => { - let batchfile = batch::File::load(&batch)?; - - parent_info = wallet.get_parent_info(batchfile.parent)?; - - (inscriptions, reveal_satpoints, postages, destinations) = batchfile.inscriptions( - &wallet, - utxos, - parent_info.as_ref().map(|info| info.tx_out.value), - self.compress, - )?; - - locked_utxos.extend( - reveal_satpoints - .iter() - .map(|(satpoint, txout)| (satpoint.outpoint, txout.clone())), - ); - - mode = batchfile.mode; - - reinscribe = batchfile.reinscribe; - - etching = batchfile.etching; - - if let Some(sat) = batchfile.sat { - Some(wallet.find_sat_in_outputs(sat)?) - } else { - batchfile.satpoint - } - } - _ => unreachable!(), - }; - - if let Some(etching) = etching { - Self::check_etching(&wallet, &etching)?; } batch::Plan { - commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), - destinations, - dry_run: self.dry_run, - etching, - inscriptions, - mode, - no_backup: self.no_backup, - no_limit: self.no_limit, - parent_info, - postages, - reinscribe, - reveal_fee_rate: self.fee_rate, - reveal_satpoints, - satpoint, + commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate), + destinations: vec![match self.destination.clone() { + Some(destination) => destination.require_network(chain.network())?, + None => wallet.get_change_address()?, + }], + dry_run: self.shared.dry_run, + etching: None, + inscriptions: vec![Inscription::from_file( + chain, + self.shared.compress, + self.delegate, + Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?, + self.metaprotocol, + self.parent.into_iter().collect(), + self.file, + None, + None, + )?], + mode: batch::Mode::SeparateOutputs, + no_backup: self.shared.no_backup, + no_limit: self.shared.no_limit, + parent_info: wallet.get_parent_info(self.parent)?, + postages: vec![self.postage.unwrap_or(TARGET_POSTAGE)], + reinscribe: self.reinscribe, + reveal_fee_rate: self.shared.fee_rate, + reveal_satpoints: Vec::new(), + satpoint: if let Some(sat) = self.sat { + Some(wallet.find_sat_in_outputs(sat)?) + } else { + self.satpoint + }, } .inscribe( - &locked_utxos.into_keys().collect(), - runic_utxos, - utxos, + &wallet.locked_utxos().clone().into_keys().collect(), + wallet.get_runic_outputs()?, + wallet.utxos(), &wallet, ) } @@ -214,622 +109,11 @@ impl Inscribe { Ok(None) } } - - fn check_etching(wallet: &Wallet, etching: &batch::Etching) -> Result { - let rune = etching.rune.rune; - - ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); - - ensure!( - etching.divisibility <= Etching::MAX_DIVISIBILITY, - " must be less than or equal 38" - ); - - ensure!( - wallet.has_rune_index(), - "etching runes requires index created with `--index-runes`", - ); - - ensure!( - wallet.get_rune(rune)?.is_none(), - "rune `{rune}` has already been etched", - ); - - let premine = etching.premine.to_integer(etching.divisibility)?; - - let supply = etching.supply.to_integer(etching.divisibility)?; - - let mintable = etching - .terms - .map(|terms| -> Result { - terms - .cap - .checked_mul(terms.amount.to_integer(etching.divisibility)?) - .ok_or_else(|| anyhow!("`terms.count` * `terms.amount` over maximum")) - }) - .transpose()? - .unwrap_or_default(); - - ensure!( - supply - == premine - .checked_add(mintable) - .ok_or_else(|| anyhow!("`premine` + `terms.count` * `terms.amount` over maximum"))?, - "`supply` not equal to `premine` + `terms.count` * `terms.amount`" - ); - - ensure!(supply > 0, "`supply` must be greater than zero"); - - let bitcoin_client = wallet.bitcoin_client(); - - let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap(); - - let reveal_height = current_height + 1 + RUNE_COMMIT_INTERVAL; - - if let Some(terms) = etching.terms { - if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) { - ensure!( - end > start, - "`terms.offset.end` must be greater than `terms.offset.start`" - ); - } - - if let Some((start, end)) = terms.height.and_then(|range| range.start.zip(range.end)) { - ensure!( - end > start, - "`terms.height.end` must be greater than `terms.height.start`" - ); - } - - if let Some(end) = terms.height.and_then(|range| range.end) { - ensure!( - end > reveal_height.into(), - "`terms.height.end` must be greater than the reveal transaction block height of {reveal_height}" - ); - } - - if let Some(start) = terms.height.and_then(|range| range.start) { - ensure!( - start > reveal_height.into(), - "`terms.height.start` must be greater than the reveal transaction block height of {reveal_height}" - ); - } - - ensure!(terms.cap > 0, "`terms.cap` must be greater than zero"); - - ensure!( - terms.amount.to_integer(etching.divisibility)? > 0, - "`terms.amount` must be greater than zero", - ); - } - - let minimum = Rune::minimum_at_height(wallet.chain().into(), Height(reveal_height)); - - ensure!( - rune >= minimum, - "rune is less than minimum for next block: {rune} < {minimum}", - ); - - Ok(()) - } } #[cfg(test)] mod tests { - use { - super::*, - crate::wallet::batch::{self, ParentInfo}, - bitcoin::policy::MAX_STANDARD_TX_WEIGHT, - serde_yaml::{Mapping, Value}, - tempfile::TempDir, - }; - - #[test] - fn reveal_transaction_pays_fee() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; - let inscription = inscription("text/plain", "ord"); - let commit_address = change(0); - let reveal_address = recipient(); - let reveal_change = [commit_address, change(1)]; - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint: Some(satpoint(1, 0)), - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - reveal_change, - change(2), - ) - .unwrap(); - - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64); - - assert_eq!( - reveal_tx.output[0].value, - 20000 - fee.to_sat() - (20000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_transactions_opt_in_to_rbf() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; - let inscription = inscription("text/plain", "ord"); - let commit_address = change(0); - let reveal_address = recipient(); - let reveal_change = [commit_address, change(1)]; - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint: Some(satpoint(1, 0)), - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - reveal_change, - change(2), - ) - .unwrap(); - - assert!(commit_tx.is_explicitly_rbf()); - assert!(reveal_tx.is_explicitly_rbf()); - } - - #[test] - fn inscribe_with_no_satpoint_and_no_cardinal_utxos() { - let utxos = vec![(outpoint(1), tx_out(1000, address()))]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let error = batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - inscriptions, - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains("wallet contains no cardinal utxos"), - "{}", - error - ); - } - - #[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())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - assert!(batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - inscriptions, - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .is_ok()) - } - - #[test] - fn inscribe_with_custom_fee_rate() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - let fee_rate = 3.3; - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize()) - .to_sat(); - - assert_eq!( - reveal_tx.output[0].value, - 20_000 - fee - (20_000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - - let mut inscriptions = BTreeMap::new(); - let parent_inscription = inscription_id(1); - let parent_info = ParentInfo { - destination: change(3), - id: parent_inscription, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - inscriptions.insert(parent_info.location, vec![parent_inscription]); - - let child_inscription = InscriptionTemplate { - parents: vec![parent_inscription], - ..default() - } - .into(); - - let commit_address = change(1); - let reveal_address = recipient(); - let fee_rate = 4.0; - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions: vec![child_inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(1), - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let sig_vbytes = 16; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize() + sig_vbytes) - .to_sat(); - - assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..default() - } - ); - } - - #[test] - fn inscribe_with_commit_fee_rate() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - let commit_fee_rate = 3.3; - let fee_rate = 1.0; - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(commit_fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(commit_fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize()) - .to_sat(); - - assert_eq!( - reveal_tx.output[0].value, - 20_000 - fee - (20_000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let error = batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402799")), - "{}", - error - ); - } - - #[test] - fn inscribe_with_no_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let batch::Transactions { reveal_tx, .. } = batch::Plan { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: true, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - change(2), - ) - .unwrap(); - - assert!(reveal_tx.size() >= MAX_STANDARD_TX_WEIGHT as usize); - } + use super::*; #[test] fn cbor_and_json_metadata_flags_conflict() { @@ -851,748 +135,6 @@ mod tests { ); } - #[test] - fn batch_is_loaded_from_yaml_file() { - let parent = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" - .parse::() - .unwrap(); - - let tempdir = TempDir::new().unwrap(); - - let inscription_path = tempdir.path().join("tulip.txt"); - fs::write(&inscription_path, "tulips are pretty").unwrap(); - - let brc20_path = tempdir.path().join("token.json"); - - let batch_path = tempdir.path().join("batch.yaml"); - fs::write( - &batch_path, - format!( - "mode: separate-outputs -parent: {parent} -inscriptions: -- file: {} - metadata: - title: Lorem Ipsum - description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante. -- file: {} - metaprotocol: brc-20 -", - inscription_path.display(), - brc20_path.display() - ), - ) - .unwrap(); - - let mut metadata = Mapping::new(); - metadata.insert( - Value::String("title".to_string()), - Value::String("Lorem Ipsum".to_string()), - ); - metadata.insert(Value::String("description".to_string()), Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.".to_string())); - - assert_eq!( - batch::File::load(&batch_path).unwrap(), - batch::File { - inscriptions: vec![ - batch::Entry { - file: inscription_path, - metadata: Some(Value::Mapping(metadata)), - ..default() - }, - batch::Entry { - file: brc20_path, - metaprotocol: Some("brc-20".to_string()), - ..default() - } - ], - parent: Some(parent), - ..default() - } - ); - } - - #[test] - fn batch_with_unknown_field_throws_error() { - let tempdir = TempDir::new().unwrap(); - let batch_path = tempdir.path().join("batch.yaml"); - fs::write( - &batch_path, - "mode: shared-output\ninscriptions:\n- file: meow.wav\nunknown: 1.)what", - ) - .unwrap(); - - assert!(batch::File::load(&batch_path) - .unwrap_err() - .to_string() - .contains("unknown field `unknown`")); - } - - #[test] - fn batch_inscribe_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(50_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - ]; - - let mode = batch::Mode::SharedOutput; - - let fee_rate = 4.0.try_into().unwrap(); - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(2), - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - let sig_vbytes = 16; - let fee = fee_rate.fee(reveal_tx.vsize() + sig_vbytes).to_sat(); - - assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..default() - } - ); - } - - #[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())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(4), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10_000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parents: vec![parent], - pointer: Some(10_000), - } - .into(), - InscriptionTemplate { - parents: vec![parent], - pointer: Some(11_111), - } - .into(), - InscriptionTemplate { - parents: vec![parent], - pointer: Some(13_3333), - } - .into(), - ]; - - let reveal_satpoints = utxos - .iter() - .take(3) - .map(|(outpoint, txout)| { - ( - SatPoint { - outpoint: *outpoint, - offset: 0, - }, - txout.clone(), - ) - }) - .collect::>(); - - let mode = batch::Mode::SatPoints; - - let fee_rate = 1.0.try_into().unwrap(); - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - reveal_satpoints: reveal_satpoints.clone(), - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - postages: vec![ - Amount::from_sat(1_111), - Amount::from_sat(2_222), - Amount::from_sat(3_333), - ], - mode, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - reveal_satpoints - .iter() - .map(|(satpoint, _)| satpoint.outpoint) - .collect(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..default() - } - ); - } - - #[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())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let inscriptions = vec![ - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let error = batch::Plan { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 4.0.try_into().unwrap(), - reveal_fee_rate: 4.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ) - .unwrap_err() - .to_string(); - - assert!(error.contains( - "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." - )); - } - - #[test] - #[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())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let inscriptions = vec![ - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient()]; - - let _ = batch::Plan { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 4.0.try_into().unwrap(), - reveal_fee_rate: 4.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000)], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ); - } - - #[test] - fn batch_inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let wallet_inscriptions = BTreeMap::new(); - - let inscriptions = vec![ - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let error = batch::Plan { - satpoint: None, - parent_info: None, - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 1.0.try_into().unwrap(), - reveal_fee_rate: 1.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(30_000); 3], - mode: batch::Mode::SharedOutput, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402841")), - "{}", - error - ); - } - - #[test] - fn batch_inscribe_into_separate_outputs() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(80_000, address())), - ]; - - let wallet_inscriptions = BTreeMap::new(); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - inscription("text/plain", [b'O'; 100]), - inscription("text/plain", [b'O'; 111]), - inscription("text/plain", [b'O'; 222]), - ]; - - let mode = batch::Mode::SeparateOutputs; - - let fee_rate = 4.0.try_into().unwrap(); - - let batch::Transactions { reveal_tx, .. } = batch::Plan { - satpoint: None, - parent_info: None, - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ) - .unwrap(); - - assert_eq!(reveal_tx.output.len(), 3); - assert!(reveal_tx - .output - .iter() - .all(|output| output.value == TARGET_POSTAGE.to_sat())); - } - - #[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())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - InscriptionTemplate { - parents: vec![parent], - ..default() - } - .into(), - ]; - - let mode = batch::Mode::SeparateOutputs; - - let fee_rate = 4.0.try_into().unwrap(); - - let batch::Transactions { - commit_tx, - reveal_tx, - .. - } = batch::Plan { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..default() - } - .create_batch_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - change(3), - ) - .unwrap(); - - assert_eq!( - vec![parent], - ParsedEnvelope::from_transaction(&reveal_tx)[0] - .payload - .parents(), - ); - assert_eq!( - vec![parent], - ParsedEnvelope::from_transaction(&reveal_tx)[1] - .payload - .parents(), - ); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..default() - } - ); - } - - #[test] - fn example_batchfile_deserializes_successfully() { - batch::File::load(Path::new("batch.yaml")).unwrap(); - } - - #[test] - fn flags_conflict_with_batch() { - for (flag, value) in [ - ("--file", Some("foo")), - ( - "--delegate", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bi0"), - ), - ( - "--destination", - Some("tb1qsgx55dp6gn53tsmyjjv4c2ye403hgxynxs0dnm"), - ), - ("--cbor-metadata", Some("foo")), - ("--json-metadata", Some("foo")), - ("--sat", Some("0")), - ( - "--satpoint", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0"), - ), - ("--reinscribe", None), - ("--metaprotocol", Some("foo")), - ( - "--parent", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bi0"), - ), - ] { - let mut args = vec![ - "ord", - "wallet", - "inscribe", - "--fee-rate", - "1", - "--batch", - "foo.yaml", - flag, - ]; - - if let Some(value) = value { - args.push(value); - } - - assert!(Arguments::try_parse_from(args) - .unwrap_err() - .to_string() - .contains("the argument '--batch ' cannot be used with")); - } - } - - #[test] - fn batch_or_file_is_required() { - assert!( - Arguments::try_parse_from(["ord", "wallet", "inscribe", "--fee-rate", "1",]) - .unwrap_err() - .to_string() - .contains("error: the following required arguments were not provided:\n <--file |--batch >") - ); - } - #[test] fn satpoint_and_sat_flags_conflict() { assert_regex_match!( diff --git a/src/subcommand/wallet/shared_args.rs b/src/subcommand/wallet/shared_args.rs new file mode 100644 index 0000000000..e9db3b5cb7 --- /dev/null +++ b/src/subcommand/wallet/shared_args.rs @@ -0,0 +1,24 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(super) struct SharedArgs { + #[arg( + long, + help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." + )] + pub(crate) commit_fee_rate: Option, + #[arg(long, help = "Compress inscription content with brotli.")] + pub(crate) compress: bool, + #[arg(long, help = "Use fee rate of sats/vB.")] + pub(crate) fee_rate: FeeRate, + #[arg(long, help = "Don't sign or broadcast transactions.")] + pub(crate) dry_run: bool, + #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")] + pub(crate) no_backup: bool, + #[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." + )] + pub(crate) no_limit: bool, +} diff --git a/src/wallet/batch.rs b/src/wallet/batch.rs index 56c8957623..d131b73101 100644 --- a/src/wallet/batch.rs +++ b/src/wallet/batch.rs @@ -62,3 +62,1121 @@ pub struct ParentInfo { pub location: SatPoint, pub tx_out: TxOut, } + +#[cfg(test)] +mod tests { + use { + super::*, + crate::wallet::batch::{self, ParentInfo}, + bitcoin::policy::MAX_STANDARD_TX_WEIGHT, + }; + + #[test] + fn reveal_transaction_pays_fee() { + let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let inscription = inscription("text/plain", "ord"); + let commit_address = change(0); + let reveal_address = recipient(); + let reveal_change = [commit_address, change(1)]; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: Some(satpoint(1, 0)), + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + reveal_change, + change(2), + ) + .unwrap(); + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64); + + assert_eq!( + reveal_tx.output[0].value, + 20000 - fee.to_sat() - (20000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_transactions_opt_in_to_rbf() { + let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let inscription = inscription("text/plain", "ord"); + let commit_address = change(0); + let reveal_address = recipient(); + let reveal_change = [commit_address, change(1)]; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: Some(satpoint(1, 0)), + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + reveal_change, + change(2), + ) + .unwrap(); + + assert!(commit_tx.is_explicitly_rbf()); + assert!(reveal_tx.is_explicitly_rbf()); + } + + #[test] + fn inscribe_with_no_satpoint_and_no_cardinal_utxos() { + let utxos = vec![(outpoint(1), tx_out(1000, address()))]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let error = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains("wallet contains no cardinal utxos"), + "{}", + error + ); + } + + #[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())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + assert!(batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .is_ok()) + } + + #[test] + fn inscribe_with_custom_fee_rate() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + let fee_rate = 3.3; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize()) + .to_sat(); + + assert_eq!( + reveal_tx.output[0].value, + 20_000 - fee - (20_000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + + let mut inscriptions = BTreeMap::new(); + let parent_inscription = inscription_id(1); + let parent_info = ParentInfo { + destination: change(3), + id: parent_inscription, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + inscriptions.insert(parent_info.location, vec![parent_inscription]); + + let child_inscription = InscriptionTemplate { + parents: vec![parent_inscription], + ..default() + } + .into(); + + let commit_address = change(1); + let reveal_address = recipient(); + let fee_rate = 4.0; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions: vec![child_inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(1), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let sig_vbytes = 16; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize() + sig_vbytes) + .to_sat(); + + assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[test] + fn inscribe_with_commit_fee_rate() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + let commit_fee_rate = 3.3; + let fee_rate = 1.0; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(commit_fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(commit_fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize()) + .to_sat(); + + assert_eq!( + reveal_tx.output[0].value, + 20_000 - fee - (20_000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_over_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let error = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402799")), + "{}", + error + ); + } + + #[test] + fn inscribe_with_no_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let batch::Transactions { reveal_tx, .. } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: true, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + assert!(reveal_tx.size() >= MAX_STANDARD_TX_WEIGHT as usize); + } + + #[test] + fn batch_inscribe_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(50_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let mode = batch::Mode::SharedOutput; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + let sig_vbytes = 16; + let fee = fee_rate.fee(reveal_tx.vsize() + sig_vbytes).to_sat(); + + assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[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())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(4), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10_000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + pointer: Some(10_000), + } + .into(), + InscriptionTemplate { + parents: vec![parent], + pointer: Some(11_111), + } + .into(), + InscriptionTemplate { + parents: vec![parent], + pointer: Some(13_3333), + } + .into(), + ]; + + let reveal_satpoints = utxos + .iter() + .take(3) + .map(|(outpoint, txout)| { + ( + SatPoint { + outpoint: *outpoint, + offset: 0, + }, + txout.clone(), + ) + }) + .collect::>(); + + let mode = batch::Mode::SatPoints; + + let fee_rate = 1.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + reveal_satpoints: reveal_satpoints.clone(), + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + postages: vec![ + Amount::from_sat(1_111), + Amount::from_sat(2_222), + Amount::from_sat(3_333), + ], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + reveal_satpoints + .iter() + .map(|(satpoint, _)| satpoint.outpoint) + .collect(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[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())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let error = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 4.0.try_into().unwrap(), + reveal_fee_rate: 4.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains( + "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." + )); + } + + #[test] + #[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())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient()]; + + let _ = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 4.0.try_into().unwrap(), + reveal_fee_rate: 4.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000)], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ); + } + + #[test] + fn batch_inscribe_over_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let wallet_inscriptions = BTreeMap::new(); + + let inscriptions = vec![ + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let error = batch::Plan { + satpoint: None, + parent_info: None, + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 1.0.try_into().unwrap(), + reveal_fee_rate: 1.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(30_000); 3], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402841")), + "{}", + error + ); + } + + #[test] + fn batch_inscribe_into_separate_outputs() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(80_000, address())), + ]; + + let wallet_inscriptions = BTreeMap::new(); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + inscription("text/plain", [b'O'; 100]), + inscription("text/plain", [b'O'; 111]), + inscription("text/plain", [b'O'; 222]), + ]; + + let mode = batch::Mode::SeparateOutputs; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { reveal_tx, .. } = batch::Plan { + satpoint: None, + parent_info: None, + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + assert_eq!(reveal_tx.output.len(), 3); + assert!(reveal_tx + .output + .iter() + .all(|output| output.value == TARGET_POSTAGE.to_sat())); + } + + #[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())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let mode = batch::Mode::SeparateOutputs; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + assert_eq!( + vec![parent], + ParsedEnvelope::from_transaction(&reveal_tx)[0] + .payload + .parents(), + ); + assert_eq!( + vec![parent], + ParsedEnvelope::from_transaction(&reveal_tx)[1] + .payload + .parents(), + ); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } +} diff --git a/tests/lib.rs b/tests/lib.rs index 9722833306..0f142b16ff 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -89,24 +89,6 @@ fn create_wallet(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_serv .run_and_deserialize_output::(); } -fn receive( - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, - ord_rpc_server: &TestServer, -) -> Address { - let address = CommandBuilder::new("wallet receive") - .bitcoin_rpc_server(bitcoin_rpc_server) - .ord_rpc_server(ord_rpc_server) - .run_and_deserialize_output::() - .addresses - .into_iter() - .next() - .unwrap(); - - address - .require_network(bitcoin_rpc_server.state().network) - .unwrap() -} - fn sats( bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer, @@ -212,7 +194,7 @@ fn batch( bitcoin_rpc_server.mine_blocks(1); let mut builder = - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) .bitcoin_rpc_server(bitcoin_rpc_server) .ord_rpc_server(ord_rpc_server); diff --git a/tests/server.rs b/tests/server.rs index 2b3c3b3f10..323ca363d9 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -118,7 +118,7 @@ fn multiple_inscriptions_appear_on_reveal_transaction_page() { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new("wallet inscribe --batch batch.yaml --fee-rate 55") + let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55") .write("inscription.txt", "Hello World") .write("meow.wav", [0; 2048]) .write( diff --git a/tests/wallet.rs b/tests/wallet.rs index 5e347c9499..fc8cb70255 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -2,6 +2,7 @@ use super::*; mod authentication; mod balance; +mod batch_command; mod cardinals; mod create; mod dump; diff --git a/tests/wallet/batch_command.rs b/tests/wallet/batch_command.rs new file mode 100644 index 0000000000..9eb67beb0b --- /dev/null +++ b/tests/wallet/batch_command.rs @@ -0,0 +1,2427 @@ +use {super::*, ord::subcommand::wallet::send}; + +fn receive( + bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, + ord_rpc_server: &TestServer, +) -> Address { + let address = CommandBuilder::new("wallet receive") + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server) + .run_and_deserialize_output::() + .addresses + .into_iter() + .next() + .unwrap(); + + address + .require_network(bitcoin_rpc_server.state().network) + .unwrap() +} + +#[test] +fn batch_inscribe_fails_if_batchfile_has_no_inscriptions() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("batch.yaml", "mode: shared-output\ninscriptions: []\n") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stderr_regex(".*batchfile must contain at least one inscription.*") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_can_create_one_inscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n metadata: 123\n metaprotocol: foo", + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); + + assert_eq!(request.status(), 200); + assert_eq!( + request.headers().get("content-type").unwrap(), + "text/plain;charset=utf-8" + ); + assert_eq!(request.text().unwrap(), "Hello World"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + r".*
metadata
\s*
\n 123\n
.*
metaprotocol
\s*
foo
.*", + ); +} + +#[test] +fn batch_inscribe_with_multiple_inscriptions() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); + assert_eq!(request.status(), 200); + assert_eq!( + request.headers().get("content-type").unwrap(), + "text/plain;charset=utf-8" + ); + assert_eq!(request.text().unwrap(), "Hello World"); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[1].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/png"); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); +} + +#[test] +fn batch_inscribe_with_multiple_inscriptions_with_parent() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("parent: {parent_id}\nmode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + r".*
parents
\s*
.*
.*", + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + r".*
parents
\s*
.*
.*", + ); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); +} + +#[test] +fn batch_inscribe_respects_dry_run_flag() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml --dry-run") + .write("inscription.txt", "Hello World") + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n", + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert!(bitcoin_rpc_server.mempool().is_empty()); + + let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); + + assert_eq!(request.status(), 404); +} + +#[test] +fn batch_in_same_output_but_different_satpoints() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + let outpoint = output.inscriptions[0].location.outpoint; + for (i, inscription) in output.inscriptions.iter().enumerate() { + assert_eq!( + inscription.location, + SatPoint { + outpoint, + offset: u64::try_from(i).unwrap() * 10_000, + } + ); + } + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:10000
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:20000
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_in_same_output_with_non_default_postage() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + let outpoint = output.inscriptions[0].location.outpoint; + + for (i, inscription) in output.inscriptions.iter().enumerate() { + assert_eq!( + inscription.location, + SatPoint { + outpoint, + offset: u64::try_from(i).unwrap() * 777, + } + ); + } + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:777
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:1554
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_in_separate_outputs_with_parent() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("parent: {parent_id}\nmode: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + let mut outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + outpoints.sort(); + outpoints.dedup(); + assert_eq!(outpoints.len(), output.inscriptions.len()); + + bitcoin_rpc_server.mine_blocks(1); + + let output_1 = output.inscriptions[0].location.outpoint; + let output_2 = output.inscriptions[1].location.outpoint; + let output_3 = output.inscriptions[2].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + output_1 + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + output_2 + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + output_3 + ), + ); +} + +#[test] +fn batch_in_separate_outputs_with_parent_and_non_default_postage() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("parent: {parent_id}\nmode: separate-outputs\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let mut outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + outpoints.sort(); + outpoints.dedup(); + assert_eq!(outpoints.len(), output.inscriptions.len()); + + bitcoin_rpc_server.mine_blocks(1); + + let output_1 = output.inscriptions[0].location.outpoint; + let output_2 = output.inscriptions[1].location.outpoint; + let output_3 = output.inscriptions[2].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + output_1 + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + output_2 + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + output_3 + ), + ); +} + +#[test] +fn batch_inscribe_fails_if_invalid_network_destination_address() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stderr_regex("error: address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 belongs to network bitcoin which is different from required regtest\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", "") + .write("batch.yaml", "mode: shared-output\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .run_and_extract_stdout(); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", "") + .write("batch.yaml", "mode: same-sat\nsat: 5000000000\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_works_with_some_destinations_set_and_others_not() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "\ +mode: separate-outputs +inscriptions: +- file: inscription.txt + destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 +- file: tulip.png +- file: meow.wav + destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k +", + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + ".* +
address
+
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
.*", + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + ".* +
address
+
{}
.*", + bitcoin_rpc_server.state().change_addresses[0], + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + ".* +
address
+
bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
.*", + ); +} + +#[test] +fn batch_same_sat() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: same-sat\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_same_sat_with_parent() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("mode: same-sat\nparent: {parent_id}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + bitcoin_rpc_server.mine_blocks(1); + + let txid = output.inscriptions[0].location.outpoint.txid; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", parent_id), + format!( + r".*
location
.*
{}:0:0
.*", + txid + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_same_sat_with_satpoint_and_reinscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + let inscription_id = output.inscriptions[0].id; + let satpoint = output.inscriptions[0].location; + + CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("mode: same-sat\nsatpoint: {}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .stderr_regex(".*error: sat at .*:0:0 already inscribed.*") + .run_and_extract_stdout(); + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("mode: same-sat\nsatpoint: {}\nreinscribe: true\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*.*.*", inscription_id, output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_inscribe_with_sat_argument_with_parent() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = + CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("parent: {parent_id}\nmode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + ord_rpc_server.assert_response_regex( + "/sat/5000111111", + format!( + ".*.*.*.*", + output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id + ), + ); +} + +#[test] +fn batch_inscribe_with_sat_arg_fails_if_wrong_mode() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .expected_stderr("error: neither `sat` nor `satpoint` can be set in `same-sat` mode\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_with_satpoint() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + + let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("mode: same-sat\nsatpoint: {txid}:0:55555\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", ) + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + ord_rpc_server.assert_response_regex( + "/sat/5000055555", + format!( + ".*.*.*.*", + output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id + ), + ); +} + +#[test] +fn batch_inscribe_with_fee_rate() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(2); + + let set_fee_rate = 1.0; + + let output = CommandBuilder::new(format!("--index-sats wallet batch --fee-rate {set_fee_rate} --batch batch.yaml")) + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + let commit_tx = &bitcoin_rpc_server.mempool()[0]; + let mut fee = 0; + for input in &commit_tx.input { + fee += bitcoin_rpc_server + .get_utxo_amount(&input.previous_output) + .unwrap() + .to_sat(); + } + for output in &commit_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / commit_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + let reveal_tx = &bitcoin_rpc_server.mempool()[1]; + let mut fee = 0; + for input in &reveal_tx.input { + fee += &commit_tx.output[input.previous_output.vout as usize].value; + } + for output in &reveal_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / reveal_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + assert_eq!( + ord::FeeRate::try_from(set_fee_rate) + .unwrap() + .fee(commit_tx.vsize() + reveal_tx.vsize()) + .to_sat(), + output.total_fees + ); +} + +#[test] +fn batch_inscribe_with_delegate_inscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + + let inscribe = CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml") + .write("inscription.txt", "INSCRIPTION") + .write( + "batch.yaml", + format!( + "mode: shared-output +inscriptions: +- delegate: {delegate} + file: inscription.txt +" + ), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscribe.inscriptions[0].id), + format!(r#".*
delegate
\s*
{delegate}
.*"#,), + ); + + ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); +} + +#[test] +fn batch_inscribe_with_non_existent_delegate_inscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; + + CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml") + .write("hello.txt", "Hello, world!") + .write( + "batch.yaml", + format!( + "mode: shared-output +inscriptions: +- delegate: {delegate} + file: hello.txt +" + ), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr(format!("error: delegate {delegate} does not exist\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_with_satpoints_with_parent() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let parent_output = + CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + let txids = bitcoin_rpc_server + .mine_blocks(3) + .iter() + .map(|block| block.txdata[0].txid()) + .collect::>(); + + let satpoint_1 = SatPoint { + outpoint: OutPoint { + txid: txids[0], + vout: 0, + }, + offset: 0, + }; + + let satpoint_2 = SatPoint { + outpoint: OutPoint { + txid: txids[1], + vout: 0, + }, + offset: 0, + }; + + let satpoint_3 = SatPoint { + outpoint: OutPoint { + txid: txids[2], + vout: 0, + }, + offset: 0, + }; + + let sat_1 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_1.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let sat_2 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_2.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let sat_3 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_3.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!( + r#" +mode: satpoints +parent: {parent_id} +inscriptions: +- file: inscription.txt + satpoint: {} +- file: tulip.png + satpoint: {} +- file: meow.wav + satpoint: {} +"#, + satpoint_1, satpoint_2, satpoint_3 + ), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", parent_id), + format!( + r".*
location
.*
{}:0:0
.*", + output.reveal + ), + ); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + + assert_eq!(outpoints.len(), output.inscriptions.len()); + + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_1.id), + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + 50 * COIN_VALUE, + sat_1, + inscription_1.location, + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_2.id), + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + 50 * COIN_VALUE, + sat_2, + inscription_2.location + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_3.id), + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + 50 * COIN_VALUE, + sat_3, + inscription_3.location + ), + ); +} + +#[test] +fn batch_inscribe_with_satpoints_with_different_sizes() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + let address_1 = receive(&bitcoin_rpc_server, &ord_rpc_server); + let address_2 = receive(&bitcoin_rpc_server, &ord_rpc_server); + let address_3 = receive(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(3); + + let outpoint_1 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_1} 25btc" + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint_2 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_2} 1btc" + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + bitcoin_rpc_server.mine_blocks(1); + + let outpoint_3 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_3} 3btc" + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + bitcoin_rpc_server.mine_blocks(1); + + let satpoint_1 = SatPoint { + outpoint: outpoint_1, + offset: 0, + }; + + let satpoint_2 = SatPoint { + outpoint: outpoint_2, + offset: 0, + }; + + let satpoint_3 = SatPoint { + outpoint: outpoint_3, + offset: 0, + }; + + let output_1 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_1.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_1.value, 25 * COIN_VALUE); + + let output_2 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_2.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_2.value, COIN_VALUE); + + let output_3 = serde_json::from_str::( + &ord_rpc_server + .json_request(format!("/output/{}", satpoint_3.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_3.value, 3 * COIN_VALUE); + + let sat_1 = output_1.sat_ranges.unwrap()[0].0; + let sat_2 = output_2.sat_ranges.unwrap()[0].0; + let sat_3 = output_3.sat_ranges.unwrap()[0].0; + + let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 5]) + .write("meow.wav", [0; 2]) + .write( + "batch.yaml", + format!( + r#" +mode: satpoints +inscriptions: +- file: inscription.txt + satpoint: {} +- file: tulip.png + satpoint: {} +- file: meow.wav + satpoint: {} +"#, + satpoint_1, satpoint_2, satpoint_3 + ), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + + assert_eq!(outpoints.len(), output.inscriptions.len()); + + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_1.id), + format!( + r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + 25 * COIN_VALUE, + sat_1, + inscription_1.location + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_2.id), + format!( + r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + COIN_VALUE, + sat_2, + inscription_2.location + ), + ); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{}", inscription_3.id), + format!( + r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + 3 * COIN_VALUE, + sat_3, + inscription_3.location + ), + ); +} + +#[test] +fn batch_inscribe_can_etch_rune() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_offset() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + amount: "1000".parse().unwrap(), + offset: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_height() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + amount: "1000".parse().unwrap(), + height: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn etch_existing_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 1, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_reserved_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune::reserved(0).unwrap(), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_sub_minimum_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune is less than minimum for next block: A < ZZQYZPATYGGX\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_requires_rune_index() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: etching runes requires index created with `--index-runes`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_divisibility_over_maximum_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 39, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: must be less than or equal 38\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_mintable_overflow_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: default(), + premine: default(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 2, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "340282366920938463463374607431768211455".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.count` * `terms.amount` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_mintable_plus_premine_overflow_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: default(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "340282366920938463463374607431768211455".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `premine` + `terms.count` * `terms.amount` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn incorrect_supply_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `supply` not equal to `premine` + `terms.count` * `terms.amount`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_offset_interval_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + amount: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.offset.end` must be greater than `terms.offset.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_height_interval_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.height.end` must be greater than `terms.height.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_start_height_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: None, + start: Some(0), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr( + "error: `terms.height.start` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_end_height_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: None, + end: Some(0), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr( + "error: `terms.height.end` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_supply_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "0".parse().unwrap(), + premine: "0".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `supply` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_cap_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 0, + height: None, + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.cap` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_amount_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: None, + amount: "0".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.amount` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn oversize_runestone_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(6402364363415443603228541259936211926 - 1), + spacers: 0b00000111_11111111_11111111_11111111, + }, + supply: u128::MAX.to_string().parse().unwrap(), + premine: (u128::MAX - 1).to_string().parse().unwrap(), + symbol: '\u{10FFFF}', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + offset: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + amount: "1".parse().unwrap(), + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: runestone greater than maximum OP_RETURN size: 125 > 82\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index acc73106fb..01161e1387 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1,6 +1,6 @@ use { super::*, - ord::subcommand::wallet::{create, inscriptions, receive, send}, + ord::subcommand::wallet::{create, inscriptions, receive}, std::ops::Deref, }; @@ -911,27 +911,29 @@ fn error_message_when_parsing_cbor_metadata_is_reasonable() { } #[test] -fn batch_inscribe_fails_if_batchfile_has_no_inscriptions() { +fn inscribe_does_not_pick_locked_utxos() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(1); + let coinbase_tx = &bitcoin_rpc_server.mine_blocks(1)[0].txdata[0]; + let outpoint = OutPoint::new(coinbase_tx.txid(), 0); - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("batch.yaml", "mode: shared-output\ninscriptions: []\n") + bitcoin_rpc_server.lock(outpoint); + + CommandBuilder::new("wallet inscribe --file hello.txt --fee-rate 1") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .stderr_regex(".*batchfile must contain at least one inscription.*") + .write("hello.txt", "HELLOWORLD") .expected_exit_code(1) + .stderr_regex("error: wallet contains no cardinal utxos\n") .run_and_extract_stdout(); } #[test] -fn batch_inscribe_can_create_one_inscription() { +fn inscribe_can_compress() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); @@ -940,37 +942,61 @@ fn batch_inscribe_can_create_one_inscription() { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n metadata: 123\n metaprotocol: foo", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + let Inscribe { inscriptions, .. } = + CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) + .write("foo.txt", [0; 350_000]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output(); + + let inscription = inscriptions[0].id; bitcoin_rpc_server.mine_blocks(1); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + ord_rpc_server.sync_server(); - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); + let client = reqwest::blocking::Client::builder() + .brotli(false) + .build() + .unwrap(); - assert_eq!(request.status(), 200); - assert_eq!( - request.headers().get("content-type").unwrap(), - "text/plain;charset=utf-8" - ); - assert_eq!(request.text().unwrap(), "Hello World"); + let response = client + .get( + ord_rpc_server + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), + ) + .send() + .unwrap(); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - r".*
metadata
\s*
\n 123\n
.*
metaprotocol
\s*
foo
.*", + assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); + assert_regex_match!( + response.text().unwrap(), + "inscription content encoding `br` is not acceptable. `Accept-Encoding` header not present" ); + + let client = reqwest::blocking::Client::builder() + .brotli(true) + .build() + .unwrap(); + + let response = client + .get( + ord_rpc_server + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), + ) + .send() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.bytes().unwrap().deref(), [0; 350_000]); } #[test] -fn batch_inscribe_with_multiple_inscriptions() { +fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); @@ -979,121 +1005,100 @@ fn batch_inscribe_with_multiple_inscriptions() { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new("wallet inscribe --batch batch.yaml --fee-rate 55") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + let Inscribe { inscriptions, .. } = + CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) + .write("foo.txt", "foo") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output(); + + let inscription = inscriptions[0].id; bitcoin_rpc_server.mine_blocks(1); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + ord_rpc_server.sync_server(); - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); - assert_eq!(request.status(), 200); - assert_eq!( - request.headers().get("content-type").unwrap(), - "text/plain;charset=utf-8" - ); - assert_eq!(request.text().unwrap(), "Hello World"); + let client = reqwest::blocking::Client::builder() + .brotli(false) + .build() + .unwrap(); - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[1].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "image/png"); + let response = client + .get( + ord_rpc_server + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), + ) + .send() + .unwrap(); - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.text().unwrap(), "foo"); } #[test] -fn batch_inscribe_with_multiple_inscriptions_with_parent() { +fn inscribe_with_sat_arg() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + bitcoin_rpc_server.mine_blocks(2); - let parent_id = parent_output.inscriptions[0].id; + let Inscribe { inscriptions, .. } = CommandBuilder::new( + "--index-sats wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1", + ) + .write("foo.txt", "FOO") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output(); - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("parent: {parent_id}\nmode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + let inscription = inscriptions[0].id; bitcoin_rpc_server.mine_blocks(1); ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - r".*
parents
\s*
.*
.*", - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - r".*
parents
\s*
.*
.*", + "/sat/5010000000", + format!(".*.*"), ); - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); + ord_rpc_server.assert_response_regex(format!("/content/{inscription}",), "FOO"); } #[test] -fn batch_inscribe_respects_dry_run_flag() { +fn inscribe_with_sat_arg_fails_if_no_index_or_not_found() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml --dry-run") - .write("inscription.txt", "Hello World") - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n", - ) + CommandBuilder::new("wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1") + .write("foo.txt", "FOO") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert!(bitcoin_rpc_server.mempool().is_empty()); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); + .expected_exit_code(1) + .expected_stderr("error: ord index must be built with `--index-sats` to use `--sat`\n") + .run_and_extract_stdout(); - assert_eq!(request.status(), 404); + CommandBuilder::new("--index-sats wallet inscribe --sat 5000000000 --file foo.txt --fee-rate 1") + .write("foo.txt", "FOO") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&TestServer::spawn_with_server_args( + &bitcoin_rpc_server, + &["--index-sats"], + &[], + )) + .expected_exit_code(1) + .expected_stderr("error: could not find sat `5000000000` in wallet outputs\n") + .run_and_extract_stdout(); } #[test] -fn batch_in_same_output_but_different_satpoints() { +fn server_can_decompress_brotli() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); @@ -1102,65 +1107,61 @@ fn batch_in_same_output_but_different_satpoints() { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + let Inscribe { inscriptions, .. } = + CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) + .write("foo.txt", [0; 350_000]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output(); - let outpoint = output.inscriptions[0].location.outpoint; - for (i, inscription) in output.inscriptions.iter().enumerate() { - assert_eq!( - inscription.location, - SatPoint { - outpoint, - offset: u64::try_from(i).unwrap() * 10_000, - } - ); - } + let inscription = inscriptions[0].id; bitcoin_rpc_server.mine_blocks(1); - let outpoint = output.inscriptions[0].location.outpoint; + ord_rpc_server.sync_server(); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); + let client = reqwest::blocking::Client::builder() + .brotli(false) + .build() + .unwrap(); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
location
.*
{}:10000
.*", - outpoint - ), - ); + let response = client + .get( + ord_rpc_server + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), + ) + .send() + .unwrap(); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
location
.*
{}:20000
.*", - outpoint - ), - ); + assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*
.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); + let test_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &["--decompress"]); + + test_server.sync_server(); + + let client = reqwest::blocking::Client::builder() + .brotli(false) + .build() + .unwrap(); + + let response = client + .get( + test_server + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), + ) + .send() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.bytes().unwrap().deref(), [0; 350_000]); } #[test] -fn batch_in_same_output_with_non_default_postage() { +fn file_inscribe_with_delegate_inscription() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); @@ -1169,370 +1170,61 @@ fn batch_in_same_output_with_non_default_postage() { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: shared-output\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - let outpoint = output.inscriptions[0].location.outpoint; + let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - for (i, inscription) in output.inscriptions.iter().enumerate() { - assert_eq!( - inscription.location, - SatPoint { - outpoint, - offset: u64::try_from(i).unwrap() * 777, - } - ); - } + let inscribe = CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file inscription.txt" + )) + .write("inscription.txt", "INSCRIPTION") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); - let outpoint = output.inscriptions[0].location.outpoint; - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), + format!("/inscription/{}", inscribe.inscriptions[0].id), + format!(r#".*
delegate
\s*
{delegate}
.*"#,), ); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
location
.*
{}:777
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
location
.*
{}:1554
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); -} - -#[test] -fn batch_in_separate_outputs_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - let parent_id = parent_output.inscriptions[0].id; - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("parent: {parent_id}\nmode: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - let mut outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - outpoints.sort(); - outpoints.dedup(); - assert_eq!(outpoints.len(), output.inscriptions.len()); - - bitcoin_rpc_server.mine_blocks(1); - - let output_1 = output.inscriptions[0].location.outpoint; - let output_2 = output.inscriptions[1].location.outpoint; - let output_3 = output.inscriptions[2].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", - output_1 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", - output_2 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", - output_3 - ), - ); -} - -#[test] -fn batch_in_separate_outputs_with_parent_and_non_default_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - let parent_id = parent_output.inscriptions[0].id; - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("parent: {parent_id}\nmode: separate-outputs\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let mut outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - outpoints.sort(); - outpoints.dedup(); - assert_eq!(outpoints.len(), output.inscriptions.len()); - - bitcoin_rpc_server.mine_blocks(1); - - let output_1 = output.inscriptions[0].location.outpoint; - let output_2 = output.inscriptions[1].location.outpoint; - let output_3 = output.inscriptions[2].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", - output_1 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", - output_2 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", - output_3 - ), - ); -} - -#[test] -fn inscribe_does_not_pick_locked_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - let coinbase_tx = &bitcoin_rpc_server.mine_blocks(1)[0].txdata[0]; - let outpoint = OutPoint::new(coinbase_tx.txid(), 0); - - bitcoin_rpc_server.lock(outpoint); - - CommandBuilder::new("wallet inscribe --file hello.txt --fee-rate 1") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .write("hello.txt", "HELLOWORLD") - .expected_exit_code(1) - .stderr_regex("error: wallet contains no cardinal utxos\n") - .run_and_extract_stdout(); -} + ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); +} #[test] -fn inscribe_can_compress() { +fn inscription_with_delegate_returns_effective_content_type() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); bitcoin_rpc_server.mine_blocks(1); + let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - let Inscribe { inscriptions, .. } = - CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) - .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; + let inscribe = CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file meow.wav" + )) + .write("meow.wav", [0; 2048]) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); - ord_rpc_server.sync_server(); - - let client = reqwest::blocking::Client::builder() - .brotli(false) - .build() - .unwrap(); + let inscription_id = inscribe.inscriptions[0].id; + let json_response = ord_rpc_server.json_request(format!("/inscription/{}", inscription_id)); - let response = client - .get( - ord_rpc_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); + let inscription_json: api::Inscription = + serde_json::from_str(&json_response.text().unwrap()).unwrap(); + assert_regex_match!(inscription_json.address.unwrap(), r"bc1p.*"); - assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); - assert_regex_match!( - response.text().unwrap(), - "inscription content encoding `br` is not acceptable. `Accept-Encoding` header not present" + assert_eq!(inscription_json.content_type, Some("audio/wav".to_string())); + assert_eq!( + inscription_json.effective_content_type, + Some("text/plain;charset=utf-8".to_string()) ); - - let client = reqwest::blocking::Client::builder() - .brotli(true) - .build() - .unwrap(); - - let response = client - .get( - ord_rpc_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.bytes().unwrap().deref(), [0; 350_000]); -} - -#[test] -fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let Inscribe { inscriptions, .. } = - CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) - .write("foo.txt", "foo") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.sync_server(); - - let client = reqwest::blocking::Client::builder() - .brotli(false) - .build() - .unwrap(); - - let response = client - .get( - ord_rpc_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.text().unwrap(), "foo"); -} - -#[test] -fn batch_inscribe_fails_if_invalid_network_destination_address() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stderr_regex("error: address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 belongs to network bitcoin which is different from required regtest\n") - .expected_exit_code(1) - .run_and_extract_stdout(); } #[test] -fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() { +fn file_inscribe_with_non_existent_delegate_inscription() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); @@ -1541,2115 +1233,15 @@ fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() { bitcoin_rpc_server.mine_blocks(1); - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", "") - .write("batch.yaml", "mode: shared-output\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") - .run_and_extract_stdout(); + let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", "") - .write("batch.yaml", "mode: same-sat\nsat: 5000000000\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_works_with_some_destinations_set_and_others_not() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --batch batch.yaml --fee-rate 55") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "\ -mode: separate-outputs -inscriptions: -- file: inscription.txt - destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 -- file: tulip.png -- file: meow.wav - destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k -", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - ".* -
address
-
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
.*", - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - ".* -
address
-
{}
.*", - bitcoin_rpc_server.state().change_addresses[0], - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - ".* -
address
-
bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
.*", - ); -} - -#[test] -fn batch_same_sat() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: same-sat\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!( - output.inscriptions[0].location, - output.inscriptions[1].location - ); - assert_eq!( - output.inscriptions[1].location, - output.inscriptions[2].location - ); - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); -} - -#[test] -fn batch_same_sat_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_id = parent_output.inscriptions[0].id; - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("mode: same-sat\nparent: {parent_id}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!( - output.inscriptions[0].location, - output.inscriptions[1].location - ); - assert_eq!( - output.inscriptions[1].location, - output.inscriptions[2].location - ); - - bitcoin_rpc_server.mine_blocks(1); - - let txid = output.inscriptions[0].location.outpoint.txid; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", parent_id), - format!( - r".*
location
.*
{}:0:0
.*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
location
.*
{}:1:0
.*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
location
.*
{}:1:0
.*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
location
.*
{}:1:0
.*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); -} - -#[test] -fn batch_same_sat_with_satpoint_and_reinscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let inscription_id = output.inscriptions[0].id; - let satpoint = output.inscriptions[0].location; - - CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("mode: same-sat\nsatpoint: {}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .stderr_regex(".*error: sat at .*:0:0 already inscribed.*") - .run_and_extract_stdout(); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("mode: same-sat\nsatpoint: {}\nreinscribe: true\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!( - output.inscriptions[0].location, - output.inscriptions[1].location - ); - assert_eq!( - output.inscriptions[1].location, - output.inscriptions[2].location - ); - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
location
.*
{}:0
.*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*.*.*.*.*.*.*.*.*", inscription_id, output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); -} - -#[test] -fn inscribe_with_sat_arg() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(2); - - let Inscribe { inscriptions, .. } = CommandBuilder::new( - "--index-sats wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1", - ) - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - "/sat/5010000000", - format!(".*.*"), - ); - - ord_rpc_server.assert_response_regex(format!("/content/{inscription}",), "FOO"); -} - -#[test] -fn inscribe_with_sat_arg_fails_if_no_index_or_not_found() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - CommandBuilder::new("wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: ord index must be built with `--index-sats` to use `--sat`\n") - .run_and_extract_stdout(); - - CommandBuilder::new("--index-sats wallet inscribe --sat 5000000000 --file foo.txt --fee-rate 1") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&TestServer::spawn_with_server_args( - &bitcoin_rpc_server, - &["--index-sats"], - &[], - )) - .expected_exit_code(1) - .expected_stderr("error: could not find sat `5000000000` in wallet outputs\n") - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_sat_argument_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = - CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - let parent_id = parent_output.inscriptions[0].id; - - let output = CommandBuilder::new("--index-sats wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("parent: {parent_id}\nmode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - "/sat/5000111111", - format!( - ".*.*.*.*", - output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id - ), - ); -} - -#[test] -fn batch_inscribe_with_sat_arg_fails_if_wrong_mode() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: shared-output\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: neither `sat` nor `satpoint` can be set in `same-sat` mode\n") - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_satpoint() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!("mode: same-sat\nsatpoint: {txid}:0:55555\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - "/sat/5000055555", - format!( - ".*.*.*.*", - output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id - ), - ); -} - -#[test] -fn batch_inscribe_with_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(2); - - let set_fee_rate = 1.0; - - let output = CommandBuilder::new(format!("--index-sats wallet inscribe --fee-rate {set_fee_rate} --batch batch.yaml")) - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - let commit_tx = &bitcoin_rpc_server.mempool()[0]; - let mut fee = 0; - for input in &commit_tx.input { - fee += bitcoin_rpc_server - .get_utxo_amount(&input.previous_output) - .unwrap() - .to_sat(); - } - for output in &commit_tx.output { - fee -= output.value; - } - let fee_rate = fee as f64 / commit_tx.vsize() as f64; - pretty_assert_eq!(fee_rate, set_fee_rate); - - let reveal_tx = &bitcoin_rpc_server.mempool()[1]; - let mut fee = 0; - for input in &reveal_tx.input { - fee += &commit_tx.output[input.previous_output.vout as usize].value; - } - for output in &reveal_tx.output { - fee -= output.value; - } - let fee_rate = fee as f64 / reveal_tx.vsize() as f64; - pretty_assert_eq!(fee_rate, set_fee_rate); - - assert_eq!( - ord::FeeRate::try_from(set_fee_rate) - .unwrap() - .fee(commit_tx.vsize() + reveal_tx.vsize()) - .to_sat(), - output.total_fees - ); -} - -#[test] -fn server_can_decompress_brotli() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let Inscribe { inscriptions, .. } = - CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) - .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.sync_server(); - - let client = reqwest::blocking::Client::builder() - .brotli(false) - .build() - .unwrap(); - - let response = client - .get( - ord_rpc_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); - - let test_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &["--decompress"]); - - test_server.sync_server(); - - let client = reqwest::blocking::Client::builder() - .brotli(false) - .build() - .unwrap(); - - let response = client - .get( - test_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.bytes().unwrap().deref(), [0; 350_000]); -} - -#[test] -fn file_inscribe_with_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - - let inscribe = CommandBuilder::new(format!( - "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file inscription.txt" - )) - .write("inscription.txt", "INSCRIPTION") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscribe.inscriptions[0].id), - format!(r#".*
delegate
\s*
{delegate}
.*"#,), - ); - - ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); -} - -#[test] -fn inscription_with_delegate_returns_effective_content_type() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - - let inscribe = CommandBuilder::new(format!( - "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file meow.wav" - )) - .write("meow.wav", [0; 2048]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let inscription_id = inscribe.inscriptions[0].id; - let json_response = ord_rpc_server.json_request(format!("/inscription/{}", inscription_id)); - - let inscription_json: api::Inscription = - serde_json::from_str(&json_response.text().unwrap()).unwrap(); - assert_regex_match!(inscription_json.address.unwrap(), r"bc1p.*"); - - assert_eq!(inscription_json.content_type, Some("audio/wav".to_string())); - assert_eq!( - inscription_json.effective_content_type, - Some("text/plain;charset=utf-8".to_string()) - ); -} - -#[test] -fn file_inscribe_with_non_existent_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; - - CommandBuilder::new(format!( - "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file child.png" - )) - .write("child.png", [1; 520]) + CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file child.png" + )) + .write("child.png", [1; 520]) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .expected_stderr(format!("error: delegate {delegate} does not exist\n")) .expected_exit_code(1) .run_and_extract_stdout(); } - -#[test] -fn batch_inscribe_with_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - - let inscribe = CommandBuilder::new("wallet inscribe --fee-rate 1.0 --batch batch.yaml") - .write("inscription.txt", "INSCRIPTION") - .write( - "batch.yaml", - format!( - "mode: shared-output -inscriptions: -- delegate: {delegate} - file: inscription.txt -" - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscribe.inscriptions[0].id), - format!(r#".*
delegate
\s*
{delegate}
.*"#,), - ); - - ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); -} - -#[test] -fn batch_inscribe_with_non_existent_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; - - CommandBuilder::new("wallet inscribe --fee-rate 1.0 --batch batch.yaml") - .write("hello.txt", "Hello, world!") - .write( - "batch.yaml", - format!( - "mode: shared-output -inscriptions: -- delegate: {delegate} - file: hello.txt -" - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr(format!("error: delegate {delegate} does not exist\n")) - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_satpoints_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = - CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let txids = bitcoin_rpc_server - .mine_blocks(3) - .iter() - .map(|block| block.txdata[0].txid()) - .collect::>(); - - let satpoint_1 = SatPoint { - outpoint: OutPoint { - txid: txids[0], - vout: 0, - }, - offset: 0, - }; - - let satpoint_2 = SatPoint { - outpoint: OutPoint { - txid: txids[1], - vout: 0, - }, - offset: 0, - }; - - let satpoint_3 = SatPoint { - outpoint: OutPoint { - txid: txids[2], - vout: 0, - }, - offset: 0, - }; - - let sat_1 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_1.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - let sat_2 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_2.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - let sat_3 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_3.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - let parent_id = parent_output.inscriptions[0].id; - - let output = CommandBuilder::new("--index-sats wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - format!( - r#" -mode: satpoints -parent: {parent_id} -inscriptions: -- file: inscription.txt - satpoint: {} -- file: tulip.png - satpoint: {} -- file: meow.wav - satpoint: {} -"#, - satpoint_1, satpoint_2, satpoint_3 - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", parent_id), - format!( - r".*
location
.*
{}:0:0
.*", - output.reveal - ), - ); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - - assert_eq!(outpoints.len(), output.inscriptions.len()); - - let inscription_1 = &output.inscriptions[0]; - let inscription_2 = &output.inscriptions[1]; - let inscription_3 = &output.inscriptions[2]; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_1.id), - format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - 50 * COIN_VALUE, - sat_1, - inscription_1.location, - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_2.id), - format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - 50 * COIN_VALUE, - sat_2, - inscription_2.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_3.id), - format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - 50 * COIN_VALUE, - sat_3, - inscription_3.location - ), - ); -} - -#[test] -fn batch_inscribe_with_satpoints_with_different_sizes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - let address_1 = receive(&bitcoin_rpc_server, &ord_rpc_server); - let address_2 = receive(&bitcoin_rpc_server, &ord_rpc_server); - let address_3 = receive(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(3); - - let outpoint_1 = OutPoint { - txid: CommandBuilder::new(format!( - "--index-sats wallet send --fee-rate 1 {address_1} 25btc" - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint_2 = OutPoint { - txid: CommandBuilder::new(format!( - "--index-sats wallet send --fee-rate 1 {address_2} 1btc" - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint_3 = OutPoint { - txid: CommandBuilder::new(format!( - "--index-sats wallet send --fee-rate 1 {address_3} 3btc" - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let satpoint_1 = SatPoint { - outpoint: outpoint_1, - offset: 0, - }; - - let satpoint_2 = SatPoint { - outpoint: outpoint_2, - offset: 0, - }; - - let satpoint_3 = SatPoint { - outpoint: outpoint_3, - offset: 0, - }; - - let output_1 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_1.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_1.value, 25 * COIN_VALUE); - - let output_2 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_2.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_2.value, COIN_VALUE); - - let output_3 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_3.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_3.value, 3 * COIN_VALUE); - - let sat_1 = output_1.sat_ranges.unwrap()[0].0; - let sat_2 = output_2.sat_ranges.unwrap()[0].0; - let sat_3 = output_3.sat_ranges.unwrap()[0].0; - - let output = CommandBuilder::new("--index-sats wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 5]) - .write("meow.wav", [0; 2]) - .write( - "batch.yaml", - format!( - r#" -mode: satpoints -inscriptions: -- file: inscription.txt - satpoint: {} -- file: tulip.png - satpoint: {} -- file: meow.wav - satpoint: {} -"#, - satpoint_1, satpoint_2, satpoint_3 - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - - assert_eq!(outpoints.len(), output.inscriptions.len()); - - let inscription_1 = &output.inscriptions[0]; - let inscription_2 = &output.inscriptions[1]; - let inscription_3 = &output.inscriptions[2]; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_1.id), - format!( - r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - 25 * COIN_VALUE, - sat_1, - inscription_1.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_2.id), - format!( - r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - COIN_VALUE, - sat_2, - inscription_2.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_3.id), - format!( - r".*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", - 3 * COIN_VALUE, - sat_3, - inscription_3.location - ), - ); -} - -#[test] -fn batch_inscribe_can_etch_rune() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let batch = batch( - &bitcoin_rpc_server, - &ord_rpc_server, - batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.jpeg".into(), - ..default() - }], - ..default() - }, - ); - - let parent = batch.inscribe.inscriptions[0].id; - - let request = ord_rpc_server.request(format!("/content/{parent}")); - - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); - assert_eq!(request.text().unwrap(), "inscription"); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{parent}"), - r".*
rune
\s*
AAAAAAAAAAAAA
.*", - ); - - ord_rpc_server.assert_response_regex( - "/rune/AAAAAAAAAAAAA", - format!( - r".*
parent
\s*
{parent}
.*" - ), - ); - - assert!(bitcoin_rpc_server.state().is_wallet_address( - &batch - .inscribe - .rune - .unwrap() - .destination - .unwrap() - .require_network(Network::Regtest) - .unwrap() - )); -} - -#[test] -fn batch_inscribe_can_etch_rune_with_offset() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let batch = batch( - &bitcoin_rpc_server, - &ord_rpc_server, - batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "10000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 9, - amount: "1000".parse().unwrap(), - offset: Some(batch::Range { - start: Some(10), - end: Some(20), - }), - height: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.jpeg".into(), - ..default() - }], - ..default() - }, - ); - - let parent = batch.inscribe.inscriptions[0].id; - - let request = ord_rpc_server.request(format!("/content/{parent}")); - - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); - assert_eq!(request.text().unwrap(), "inscription"); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{parent}"), - r".*
rune
\s*
AAAAAAAAAAAAA
.*", - ); - - ord_rpc_server.assert_response_regex( - "/rune/AAAAAAAAAAAAA", - format!( - r".*
parent
\s*
{parent}
.*" - ), - ); - - assert!(bitcoin_rpc_server.state().is_wallet_address( - &batch - .inscribe - .rune - .unwrap() - .destination - .unwrap() - .require_network(Network::Regtest) - .unwrap() - )); -} - -#[test] -fn batch_inscribe_can_etch_rune_with_height() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let batch = batch( - &bitcoin_rpc_server, - &ord_rpc_server, - batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "10000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 9, - amount: "1000".parse().unwrap(), - height: Some(batch::Range { - start: Some(10), - end: Some(20), - }), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.jpeg".into(), - ..default() - }], - ..default() - }, - ); - - let parent = batch.inscribe.inscriptions[0].id; - - let request = ord_rpc_server.request(format!("/content/{parent}")); - - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); - assert_eq!(request.text().unwrap(), "inscription"); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{parent}"), - r".*
rune
\s*
AAAAAAAAAAAAA
.*", - ); - - ord_rpc_server.assert_response_regex( - "/rune/AAAAAAAAAAAAA", - format!( - r".*
parent
\s*
{parent}
.*" - ), - ); - - assert!(bitcoin_rpc_server.state().is_wallet_address( - &batch - .inscribe - .rune - .unwrap() - .destination - .unwrap() - .require_network(Network::Regtest) - .unwrap() - )); -} - -#[test] -fn etch_existing_rune_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 1, - }, - supply: "1000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_reserved_rune_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune::reserved(0).unwrap(), - spacers: 0, - }, - premine: "1000".parse().unwrap(), - supply: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_sub_minimum_rune_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(0), - spacers: 0, - }, - supply: "1000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune is less than minimum for next block: A < ZZQYZPATYGGX\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_requires_rune_index() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: etching runes requires index created with `--index-runes`\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_divisibility_over_maximum_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 39, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1000".parse().unwrap(), - premine: "1000".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: must be less than or equal 38\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_mintable_overflow_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: default(), - premine: default(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 2, - offset: Some(batch::Range { - end: Some(2), - start: None, - }), - amount: "340282366920938463463374607431768211455".parse().unwrap(), - height: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `terms.count` * `terms.amount` over maximum\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn etch_mintable_plus_premine_overflow_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: default(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - offset: Some(batch::Range { - end: Some(2), - start: None, - }), - amount: "340282366920938463463374607431768211455".parse().unwrap(), - height: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `premine` + `terms.count` * `terms.amount` over maximum\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn incorrect_supply_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - offset: Some(batch::Range { - end: Some(2), - start: None, - }), - amount: "1".parse().unwrap(), - height: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `supply` not equal to `premine` + `terms.count` * `terms.amount`\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn zero_offset_interval_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "2".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - offset: Some(batch::Range { - end: Some(2), - start: Some(2), - }), - amount: "1".parse().unwrap(), - height: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `terms.offset.end` must be greater than `terms.offset.start`\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn zero_height_interval_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "2".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - height: Some(batch::Range { - end: Some(2), - start: Some(2), - }), - amount: "1".parse().unwrap(), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `terms.height.end` must be greater than `terms.height.start`\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn invalid_start_height_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "2".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - height: Some(batch::Range { - end: None, - start: Some(0), - }), - amount: "1".parse().unwrap(), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr( - "error: `terms.height.start` must be greater than the reveal transaction block height of 8\n", - ) - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn invalid_end_height_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "2".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - height: Some(batch::Range { - start: None, - end: Some(0), - }), - amount: "1".parse().unwrap(), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr( - "error: `terms.height.end` must be greater than the reveal transaction block height of 8\n", - ) - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn zero_supply_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "0".parse().unwrap(), - premine: "0".parse().unwrap(), - symbol: '¢', - terms: None, - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `supply` must be greater than zero\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn zero_cap_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 0, - height: None, - amount: "1".parse().unwrap(), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `terms.cap` must be greater than zero\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn zero_amount_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - supply: "1".parse().unwrap(), - premine: "1".parse().unwrap(), - symbol: '¢', - terms: Some(batch::Terms { - cap: 1, - height: None, - amount: "0".parse().unwrap(), - offset: None, - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `terms.amount` must be greater than zero\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn oversize_runestone_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") - .write("inscription.txt", "foo") - .write( - "batch.yaml", - serde_yaml::to_string(&batch::File { - etching: Some(batch::Etching { - divisibility: 0, - rune: SpacedRune { - rune: Rune(6402364363415443603228541259936211926 - 1), - spacers: 0b00000111_11111111_11111111_11111111, - }, - supply: u128::MAX.to_string().parse().unwrap(), - premine: (u128::MAX - 1).to_string().parse().unwrap(), - symbol: '\u{10FFFF}', - terms: Some(batch::Terms { - cap: 1, - height: Some(batch::Range { - start: Some(u64::MAX - 1), - end: Some(u64::MAX), - }), - offset: Some(batch::Range { - start: Some(u64::MAX - 1), - end: Some(u64::MAX), - }), - amount: "1".parse().unwrap(), - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.txt".into(), - ..default() - }], - ..default() - }) - .unwrap(), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: runestone greater than maximum OP_RETURN size: 125 > 82\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 6bd13c7435..9c074a1527 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -318,7 +318,7 @@ fn splitting_merged_inscriptions_is_possible() { bitcoin_rpc_server.mine_blocks(1); - let inscribe = CommandBuilder::new("wallet inscribe --fee-rate 0 --batch batch.yaml") + let inscribe = CommandBuilder::new("wallet batch --fee-rate 0 --batch batch.yaml") .write("inscription.txt", "INSCRIPTION") .write( "batch.yaml",