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
2 changes: 2 additions & 0 deletions batch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ 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

- 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 @@ -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,
Expand All @@ -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::<Result<Vec<Address>>>()?;
}
_ => unreachable!(),
}
Expand Down
29 changes: 27 additions & 2 deletions src/subcommand/wallet/inscribe/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address<NetworkUnchecked>>,
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>,
Expand Down Expand Up @@ -570,14 +571,26 @@ 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());

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
Expand All @@ -588,6 +601,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 @@ -602,9 +616,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))
}
}
4 changes: 4 additions & 0 deletions test-bitcoincore-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ impl Handle {
pub fn loaded_wallets(&self) -> BTreeSet<String> {
self.state().loaded_wallets.clone()
}

pub fn get_change_addresses(&self) -> Vec<Address> {
self.state().change_addresses.clone()
}
}

impl Drop for Handle {
Expand Down
1 change: 1 addition & 0 deletions test-bitcoincore-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions test-bitcoincore-rpc/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::*;

pub(crate) struct State {
pub(crate) blocks: BTreeMap<BlockHash, Block>,
pub(crate) change_addresses: Vec<Address>,
pub(crate) descriptors: Vec<String>,
pub(crate) fail_lock_unspent: bool,
pub(crate) hashes: Vec<BlockHash>,
Expand Down Expand Up @@ -29,6 +30,7 @@ impl State {

Self {
blocks,
change_addresses: Vec::new(),
descriptors: Vec::new(),
fail_lock_unspent,
hashes,
Expand Down
91 changes: 91 additions & 0 deletions tests/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: destination field cannot be used 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::<Inscribe>();

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),
".*
<dt>address</dt>
<dd class=monospace>bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4</dd>.*",
);

ord_server.assert_response_regex(
format!("/inscription/{}", output.inscriptions[1].id),
format!(
".*
<dt>address</dt>
<dd class=monospace>{}</dd>.*",
rpc_server.get_change_addresses()[0]
),
);

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