Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add destination field to batch #2701

Merged
merged 10 commits into from
Nov 21, 2023
3 changes: 3 additions & 0 deletions batch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ 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
destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
raphjaph marked this conversation as resolved.
Show resolved Hide resolved

- file: tulip.png
metadata:
author: Satoshi Nakamoto
destination: bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6
12 changes: 2 additions & 10 deletions src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ impl Inscribe {

parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain)?;

inscriptions = batchfile.inscriptions(
(inscriptions, destinations) = batchfile.inscriptions(
&client,
chain,
parent_info.as_ref().map(|info| info.tx_out.value),
metadata,
Expand All @@ -162,15 +163,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::<Result<Vec<Address>>>()?;
}
_ => unreachable!(),
}
Expand Down
26 changes: 24 additions & 2 deletions src/subcommand/wallet/inscribe/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ pub(crate) struct BatchEntry {
pub(crate) file: PathBuf,
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) metadata: Option<serde_yaml::Value>,
pub(crate) metaprotocol: Option<String>,
pub(crate) destination: Option<Address<NetworkUnchecked>>,
}

impl BatchEntry {
Expand Down Expand Up @@ -568,13 +569,22 @@ impl Batchfile {

pub(crate) fn inscriptions(
&self,
client: &Client,
chain: Chain,
parent_value: Option<u64>,
metadata: Option<Vec<u8>>,
postage: Amount,
compress: bool,
) -> Result<Vec<Inscription>> {
) -> Result<(Vec<Inscription>, Vec<Address>)> {
assert!(!self.inscriptions.is_empty());
assert!(
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
!(self
.inscriptions
.iter()
.any(|entry| entry.destination.is_some())
&& self.mode == Mode::SharedOutput),
"invariant: destination field cannot be used in shared-output mode"
);

if metadata.is_some() {
assert!(self
Expand All @@ -586,6 +596,7 @@ impl Batchfile {
let mut pointer = parent_value.unwrap_or_default();

let mut inscriptions = Vec::new();
let mut destinations = Vec::new();
for (i, entry) in self.inscriptions.iter().enumerate() {
inscriptions.push(Inscription::from_file(
chain,
Expand All @@ -600,9 +611,20 @@ impl Batchfile {
compress,
)?);

if !(self.mode == Mode::SharedOutput && i >= 1) {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
destinations.push(entry.destination.as_ref().map_or_else(
|| get_change_address(client, chain),
|address| {
address
.clone()
.require_network(chain.network())
.map_err(|e| e.into())
},
)?);
}
pointer += postage.to_sat();
}

Ok(inscriptions)
Ok((inscriptions, destinations))
}
}
83 changes: 83 additions & 0 deletions tests/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1343,3 +1343,86 @@ 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_destination_address() {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
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("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t")
.rpc_server(&rpc_server)
.stderr_regex(".*bech32 address encoding error.*")
.expected_exit_code(1)
.run_and_extract_stdout();
}

#[test]
#[should_panic(expected = "invariant: destination field cannot be used in shared-output mode")]
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)
.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::<Inscribe>();

rpc_server.mine_blocks(1);

assert_eq!(rpc_server.descriptors().len(), 3);

TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex(
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
format!("/inscription/{}", output.inscriptions[0].id),
".*
<dt>address</dt>
<dd class=monospace>bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4</dd>.*",
);

TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex(
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
format!("/inscription/{}", output.inscriptions[1].id),
".*
<dt>address</dt>
<dd class=monospace>.*</dd>.*",
);

TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex(
format!("/inscription/{}", output.inscriptions[2].id),
".*
<dt>address</dt>
<dd class=monospace>bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k</dd>.*",
);
}
Loading