From dd56f2c5a187f1347756cc140646c80920530781 Mon Sep 17 00:00:00 2001 From: raph Date: Tue, 21 Nov 2023 03:22:09 +0100 Subject: [PATCH] Add destination field to batch (#2701) --- batch.yaml | 2 + src/subcommand/wallet/inscribe.rs | 12 +--- src/subcommand/wallet/inscribe/batch.rs | 36 +++++++++- test-bitcoincore-rpc/src/lib.rs | 4 ++ test-bitcoincore-rpc/src/server.rs | 1 + test-bitcoincore-rpc/src/state.rs | 2 + tests/wallet/inscribe.rs | 91 +++++++++++++++++++++++++ 7 files changed, 136 insertions(+), 12 deletions(-) diff --git a/batch.yaml b/batch.yaml index 29a972b6b0..ffe29ac596 100644 --- a/batch.yaml +++ b/batch.yaml @@ -28,6 +28,7 @@ inscriptions: metus est et odio. Nullam venenatis, urna et molestie vestibulum, orci mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum dolor et luctus euismod. + destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 - file: token.json metaprotocol: brc-20 @@ -35,3 +36,4 @@ inscriptions: - file: tulip.png metadata: author: Satoshi Nakamoto + destination: bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6 diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 6cf74f9b76..b12b5e67a8 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -162,7 +162,8 @@ impl Inscribe { .map(Amount::from_sat) .unwrap_or(TransactionBuilder::TARGET_POSTAGE); - inscriptions = batchfile.inscriptions( + (inscriptions, destinations) = batchfile.inscriptions( + &client, chain, parent_info.as_ref().map(|info| info.tx_out.value), metadata, @@ -171,15 +172,6 @@ impl Inscribe { )?; mode = batchfile.mode; - - let destination_count = match batchfile.mode { - Mode::SharedOutput => 1, - Mode::SeparateOutputs => inscriptions.len(), - }; - - destinations = (0..destination_count) - .map(|_| get_change_address(&client, chain)) - .collect::>>()?; } _ => unreachable!(), } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 093c9a4d7b..cc38055cc5 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -530,6 +530,7 @@ pub(crate) enum Mode { #[derive(Deserialize, Default, PartialEq, Debug, Clone)] #[serde(deny_unknown_fields)] pub(crate) struct BatchEntry { + pub(crate) destination: Option>, pub(crate) file: PathBuf, pub(crate) metadata: Option, pub(crate) metaprotocol: Option, @@ -570,14 +571,26 @@ impl Batchfile { pub(crate) fn inscriptions( &self, + client: &Client, chain: Chain, parent_value: Option, metadata: Option>, postage: Amount, compress: bool, - ) -> Result> { + ) -> Result<(Vec, Vec
)> { assert!(!self.inscriptions.is_empty()); + if self + .inscriptions + .iter() + .any(|entry| entry.destination.is_some()) + && self.mode == Mode::SharedOutput + { + return Err(anyhow!( + "individual inscription destinations cannot be set in shared-output mode" + )); + } + if metadata.is_some() { assert!(self .inscriptions @@ -605,6 +618,25 @@ impl Batchfile { pointer += postage.to_sat(); } - Ok(inscriptions) + let destinations = match self.mode { + Mode::SharedOutput => vec![get_change_address(client, chain)?], + Mode::SeparateOutputs => self + .inscriptions + .iter() + .map(|entry| { + entry.destination.as_ref().map_or_else( + || get_change_address(client, chain), + |address| { + address + .clone() + .require_network(chain.network()) + .map_err(|e| e.into()) + }, + ) + }) + .collect::, _>>()?, + }; + + Ok((inscriptions, destinations)) } } diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index b29cc95d98..56c50e45a8 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -242,6 +242,10 @@ impl Handle { pub fn loaded_wallets(&self) -> BTreeSet { self.state().loaded_wallets.clone() } + + pub fn get_change_addresses(&self) -> Vec
{ + self.state().change_addresses.clone() + } } impl Drop for Handle { diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index 96d8ae83aa..006f04b028 100644 --- a/test-bitcoincore-rpc/src/server.rs +++ b/test-bitcoincore-rpc/src/server.rs @@ -479,6 +479,7 @@ impl Api for Server { let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); let address = Address::p2tr(&secp256k1, public_key, None, self.network); + self.state().change_addresses.push(address.clone()); Ok(address) } diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index b2d09b0110..870794c471 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -2,6 +2,7 @@ use super::*; pub(crate) struct State { pub(crate) blocks: BTreeMap, + pub(crate) change_addresses: Vec
, pub(crate) descriptors: Vec, pub(crate) fail_lock_unspent: bool, pub(crate) hashes: Vec, @@ -29,6 +30,7 @@ impl State { Self { blocks, + change_addresses: Vec::new(), descriptors: Vec::new(), fail_lock_unspent, hashes, diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index f3c29e7004..3f189ffb1b 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1343,3 +1343,94 @@ fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.text().unwrap(), "foo"); } + +#[test] +fn batch_inscribe_fails_if_invalid_network_destination_address() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 0); + + create_wallet(&rpc_server); + + 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") + .rpc_server(&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_and_destination_set() { + let rpc_server = test_bitcoincore_rpc::spawn(); + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 0); + + create_wallet(&rpc_server); + + 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") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .stderr_regex("error: individual inscription destinations cannot be set in shared-output mode\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_works_with_some_destinations_set_and_others_not() { + let rpc_server = test_bitcoincore_rpc::spawn(); + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 0); + + create_wallet(&rpc_server); + + 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\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png\n- file: meow.wav\n destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k\n" + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 3); + + let ord_server = TestServer::spawn_with_args(&rpc_server, &[]); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + ".* +
address
+
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
.*", + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + ".* +
address
+
{}
.*", + rpc_server.get_change_addresses()[0] + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + ".* +
address
+
bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
.*", + ); +}