diff --git a/batch.yaml b/batch.yaml index b0b6cd5133..1cff7ca5a7 100644 --- a/batch.yaml +++ b/batch.yaml @@ -16,29 +16,28 @@ postage: 12345 # sat: 5000000000 # inscriptions to inscribe -# -# each inscription has the following fields: -# -# `file`: path to inscription contents -# `metadata`: inscription metadata (optional) -# `metaprotocol`: inscription metaprotocol (optional) -# `destination`: destination for that inscription (optional). Note: If no destination is specified a new wallet change address will be used inscriptions: - - file: mango.avif - metadata: - title: Delicious Mangos - description: > - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam semper, - ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel blandit - 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 + # path to inscription content +- file: mango.avif + # inscription to delegate content to (optional) + delegate: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0 + # destination (optional, if no destination is specified a new wallet change address will be used) + destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 + # inscription metadata (optional) + metadata: + title: Delicious Mangos + description: > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam semper, + ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel blandit + 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. + # inscription metaprotocol (optional) + metaprotocol: DOPEPROTOCOL-42069 - - file: token.json - metaprotocol: brc-20 +- file: token.json - - file: tulip.png - metadata: - author: Satoshi Nakamoto - destination: bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6 +- file: tulip.png + destination: bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6 + metadata: + author: Satoshi Nakamoto diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index d96987c168..271fa75efd 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -41,12 +41,13 @@ impl Inscription { pub(crate) fn from_file( chain: Chain, - path: impl AsRef, + compress: bool, + delegate: Option, + metadata: Option>, + metaprotocol: Option, parent: Option, + path: impl AsRef, pointer: Option, - metaprotocol: Option, - metadata: Option>, - compress: bool, ) -> Result { let path = path.as_ref(); @@ -99,11 +100,12 @@ impl Inscription { Ok(Self { body: Some(body), - content_type: Some(content_type.into()), content_encoding, + content_type: Some(content_type.into()), + delegate: delegate.map(|delegate| delegate.value()), metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), - parent: parent.map(|id| id.value()), + parent: parent.map(|parent| parent.value()), pointer: pointer.map(Self::pointer_value), ..Default::default() }) @@ -727,19 +729,29 @@ mod tests { write!(file, "foo").unwrap(); - let inscription = - Inscription::from_file(Chain::Mainnet, file.path(), None, None, None, None, false).unwrap(); + let inscription = Inscription::from_file( + Chain::Mainnet, + false, + None, + None, + None, + None, + file.path(), + None, + ) + .unwrap(); assert_eq!(inscription.pointer, None); let inscription = Inscription::from_file( Chain::Mainnet, - file.path(), + false, None, - Some(0), None, None, - false, + None, + file.path(), + Some(0), ) .unwrap(); @@ -747,12 +759,13 @@ mod tests { let inscription = Inscription::from_file( Chain::Mainnet, - file.path(), + false, None, - Some(1), None, None, - false, + None, + file.path(), + Some(1), ) .unwrap(); @@ -760,12 +773,13 @@ mod tests { let inscription = Inscription::from_file( Chain::Mainnet, - file.path(), + false, None, - Some(256), None, None, - false, + None, + file.path(), + Some(256), ) .unwrap(); diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index effada817d..fe1bc73812 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -109,6 +109,7 @@ impl Preview { cbor_metadata: None, commit_fee_rate: None, compress: false, + delegate: None, destination: None, dry_run: false, fee_rate: FeeRate::try_from(1.0).unwrap(), @@ -120,8 +121,8 @@ impl Preview { parent: None, postage: Some(TARGET_POSTAGE), reinscribe: false, - satpoint: None, sat: None, + satpoint: None, }), }), } @@ -142,6 +143,7 @@ impl Preview { cbor_metadata: None, commit_fee_rate: None, compress: false, + delegate: None, destination: None, dry_run: false, fee_rate: FeeRate::try_from(1.0).unwrap(), @@ -153,8 +155,8 @@ impl Preview { parent: None, postage: Some(TARGET_POSTAGE), reinscribe: false, - satpoint: None, sat: None, + satpoint: None, }), }), } diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 39c96a6fbf..a71271575e 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -37,6 +37,7 @@ pub(crate) struct Wallet { } #[derive(Debug, Parser)] +#[allow(clippy::large_enum_variant)] pub(crate) enum Subcommand { #[command(about = "Get wallet balance")] Balance, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index a27f88e438..7c036b5bf7 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -52,7 +52,8 @@ pub(crate) struct Inscribe { long, help = "Inscribe multiple inscriptions defined in a yaml .", conflicts_with_all = &[ - "cbor_metadata", "destination", "file", "json_metadata", "metaprotocol", "parent", "postage", "reinscribe", "satpoint" + "cbor_metadata", "delegate", "destination", "file", "json_metadata", "metaprotocol", + "parent", "postage", "reinscribe", "satpoint" ] )] pub(crate) batch: Option, @@ -69,6 +70,8 @@ pub(crate) struct Inscribe { 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.")] @@ -138,14 +141,22 @@ impl Inscribe { postage = self.postage.unwrap_or(TARGET_POSTAGE); + if let Some(delegate) = self.delegate { + ensure! { + index.inscription_exists(delegate)?, + "delegate {delegate} does not exist" + } + } + inscriptions = vec![Inscription::from_file( chain, - file, + self.compress, + self.delegate, + metadata, + self.metaprotocol, self.parent, + file, None, - self.metaprotocol, - metadata, - self.compress, )?]; mode = Mode::SeparateOutputs; @@ -168,6 +179,7 @@ impl Inscribe { .unwrap_or(TARGET_POSTAGE); (inscriptions, destinations) = batchfile.inscriptions( + &index, &client, chain, parent_info.as_ref().map(|info| info.tx_out.value), @@ -1327,6 +1339,10 @@ inscriptions: fn flags_conflict_with_batch() { for (flag, value) in [ ("--file", Some("foo")), + ( + "--delegate", + Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bi0"), + ), ( "--destination", Some("tb1qsgx55dp6gn53tsmyjjv4c2ye403hgxynxs0dnm"), diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 31671effa0..f22092d5ba 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -16,8 +16,8 @@ pub(super) struct Batch { } impl Default for Batch { - fn default() -> Batch { - Batch { + fn default() -> Self { + Self { commit_fee_rate: 1.0.try_into().unwrap(), destinations: Vec::new(), dry_run: false, @@ -543,6 +543,7 @@ pub(crate) enum Mode { #[derive(Deserialize, Default, PartialEq, Debug, Clone)] #[serde(deny_unknown_fields)] pub(crate) struct BatchEntry { + pub(crate) delegate: Option, pub(crate) destination: Option>, pub(crate) file: PathBuf, pub(crate) metadata: Option, @@ -585,6 +586,7 @@ impl Batchfile { pub(crate) fn inscriptions( &self, + index: &Index, client: &Client, chain: Chain, parent_value: Option, @@ -616,17 +618,25 @@ impl Batchfile { let mut inscriptions = Vec::new(); for (i, entry) in self.inscriptions.iter().enumerate() { + if let Some(delegate) = entry.delegate { + ensure! { + index.inscription_exists(delegate)?, + "delegate {delegate} does not exist" + } + } + inscriptions.push(Inscription::from_file( chain, - &entry.file, - self.parent, - if i == 0 { None } else { Some(pointer) }, - entry.metaprotocol.clone(), + compress, + entry.delegate, match &metadata { Some(metadata) => Some(metadata.clone()), None => entry.metadata()?, }, - compress, + entry.metaprotocol.clone(), + self.parent, + &entry.file, + if i == 0 { None } else { Some(pointer) }, )?); pointer += postage.to_sat(); diff --git a/tests/lib.rs b/tests/lib.rs index 96e788456d..273d83b4bd 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -41,15 +41,13 @@ use { }; macro_rules! assert_regex_match { - ($string:expr, $pattern:expr $(,)?) => { + ($value:expr, $pattern:expr $(,)?) => { let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap(); - let string = $string; + let string = $value.to_string(); if !regex.is_match(string.as_ref()) { - panic!( - "Regex:\n\n{}\n\n…did not match string:\n\n{}", - regex, string - ); + eprintln!("Regex did not match:"); + pretty_assert_eq!(regex.as_str(), string); } }; } diff --git a/tests/test_server.rs b/tests/test_server.rs index 7541264411..b31de49acb 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -89,6 +89,18 @@ impl TestServer { assert_regex_match!(response.text().unwrap(), regex.as_ref()); } + pub(crate) fn assert_response(&self, path: impl AsRef, expected_response: &str) { + self.sync_server(); + let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); + assert_eq!( + response.status(), + StatusCode::OK, + "{}", + response.text().unwrap() + ); + pretty_assert_eq!(response.text().unwrap(), expected_response); + } + pub(crate) fn request(&self, path: impl AsRef) -> Response { self.sync_server(); diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 91a9b6a537..2158fe3ffd 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1800,3 +1800,107 @@ fn server_can_decompress_brotli() { assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.bytes().unwrap().deref(), [0; 350_000]); } + +#[test] +fn file_inscribe_with_delegate_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let (delegate, _) = inscribe(&rpc_server); + + let inscribe = CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file inscription.txt" + )) + .write("inscription.txt", "INSCRIPTION") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( + format!("/inscription/{}", inscribe.inscriptions[0].id), + format!(r#".*
delegate
\s*
{delegate}
.*"#,), + ); + + TestServer::spawn_with_args(&rpc_server, &[]) + .assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); +} + +#[test] +fn file_inscribe_with_non_existent_delegate_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + 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]) + .rpc_server(&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 rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let (delegate, _) = inscribe(&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 +" + ), + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( + format!("/inscription/{}", inscribe.inscriptions[0].id), + format!(r#".*
delegate
\s*
{delegate}
.*"#,), + ); + + TestServer::spawn_with_args(&rpc_server, &[]) + .assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); +} + +#[test] +fn batch_inscribe_with_non_existent_delegate_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + 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 +" + ), + ) + .rpc_server(&rpc_server) + .expected_stderr(format!("error: delegate {delegate} does not exist\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); +}