Skip to content

Commit

Permalink
Add destination field to batch (#2701)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph authored Nov 21, 2023
1 parent 1b0bde9 commit dd56f2c
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 12 deletions.
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
36 changes: 34 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,
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 Down Expand Up @@ -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::<Result<Vec<_>, _>>()?,
};

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: 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::<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>.*",
);
}

0 comments on commit dd56f2c

Please sign in to comment.