diff --git a/src/error.rs b/src/error.rs index e33d0689f1..6a30013ab2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -74,6 +74,8 @@ pub enum SnafuError { path: PathBuf, source: io::Error, }, + #[snafu(display("Unrecognized signer: `{}`", input))] + SignerParse { input: String }, } impl From for SnafuError { diff --git a/src/lib.rs b/src/lib.rs index acc628e8d7..2a2fb7b27c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ use { outgoing::Outgoing, representation::Representation, settings::Settings, + signer::Signer, subcommand::{OutputFormat, Subcommand, SubcommandResult}, tally::Tally, }, @@ -125,6 +126,7 @@ mod re; mod representation; pub mod runes; pub mod settings; +mod signer; pub mod subcommand; mod tally; pub mod templates; diff --git a/src/signer.rs b/src/signer.rs new file mode 100644 index 0000000000..f9b1eab684 --- /dev/null +++ b/src/signer.rs @@ -0,0 +1,36 @@ +use super::*; + +#[derive(Debug, PartialEq, Clone, DeserializeFromStr)] +pub(crate) enum Signer { + Address(Address), + Inscription(InscriptionId), + Output(OutPoint), +} + +impl FromStr for Signer { + type Err = SnafuError; + + fn from_str(input: &str) -> Result { + if re::ADDRESS.is_match(input) { + Ok(Signer::Address( + input.parse().snafu_context(error::AddressParse { input })?, + )) + } else if re::OUTPOINT.is_match(input) { + Ok(Signer::Output( + input + .parse() + .snafu_context(error::OutPointParse { input })?, + )) + } else if re::INSCRIPTION_ID.is_match(input) { + Ok(Signer::Inscription( + input + .parse() + .snafu_context(error::InscriptionIdParse { input })?, + )) + } else { + Err(SnafuError::SignerParse { + input: input.to_string(), + }) + } + } +} diff --git a/src/subcommand/verify.rs b/src/subcommand/verify.rs index 75f2e3bd5a..ad7699a12a 100644 --- a/src/subcommand/verify.rs +++ b/src/subcommand/verify.rs @@ -8,7 +8,7 @@ use { group( ArgGroup::new("input") .required(true) - .args(&["message", "file"])), + .args(&["text", "file"])), group( ArgGroup::new("signature") .required(true) @@ -17,8 +17,8 @@ group( pub(crate) struct Verify { #[arg(long, help = "Verify signature made by
.")] address: Address, - #[arg(long, help = "Verify signature over .")] - message: Option, + #[arg(long, help = "Verify signature over .")] + text: Option, #[arg(long, help = "Verify signature over contents of .")] file: Option, #[arg(long, help = "Verify base64-encoded .")] @@ -29,8 +29,8 @@ pub(crate) struct Verify { impl Verify { pub(crate) fn run(self) -> SubcommandResult { - let message = if let Some(message) = &self.message { - message.as_bytes() + let message = if let Some(text) = &self.text { + text.as_bytes() } else if let Some(file) = &self.file { &fs::read(file)? } else { diff --git a/src/subcommand/wallet/sign.rs b/src/subcommand/wallet/sign.rs index 4da96578da..70afdef681 100644 --- a/src/subcommand/wallet/sign.rs +++ b/src/subcommand/wallet/sign.rs @@ -6,41 +6,60 @@ use { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { pub address: Address, - pub message: Option, pub witness: String, } #[derive(Debug, Parser)] -#[clap(group( +#[clap( +group( ArgGroup::new("input") .required(true) - .args(&["message", "file"])) -)] + .args(&["text", "file"]))) +] pub(crate) struct Sign { - #[arg(long, help = "Sign for
.")] - address: Address, - #[arg(long, help = "Sign .")] - message: Option, + #[arg( + long, + help = "Sign with public key associated with address, output, or inscription." + )] + signer: Signer, + #[arg(long, help = "Sign .")] + text: Option, #[arg(long, help = "Sign contents of .")] file: Option, } impl Sign { pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult { - let address = &self - .address - .clone() - .require_network(wallet.chain().network())?; + let address = match &self.signer { + Signer::Address(address) => address.clone().require_network(wallet.chain().network())?, + Signer::Inscription(inscription) => Address::from_str( + &wallet + .inscription_info() + .get(inscription) + .ok_or_else(|| anyhow!("inscription {inscription} not in wallet"))? + .address + .clone() + .ok_or_else(|| anyhow!("inscription {inscription} in output without address"))?, + )? + .require_network(wallet.chain().network())?, + Signer::Output(output) => wallet.chain().address_from_script( + &wallet + .utxos() + .get(output) + .ok_or_else(|| anyhow!("output {output} has no address"))? + .script_pubkey, + )?, + }; - let message = if let Some(message) = &self.message { - message.as_bytes() + let message = if let Some(text) = &self.text { + text.as_bytes() } else if let Some(file) = &self.file { &fs::read(file)? } else { unreachable!() }; - let to_spend = bip322::create_to_spend(address, message)?; + let to_spend = bip322::create_to_spend(&address, message)?; let to_sign = bip322::create_to_sign(&to_spend, None)?; @@ -64,7 +83,6 @@ impl Sign { Ok(Some(Box::new(Output { address: address.as_unchecked().clone(), - message: self.message.clone(), witness: general_purpose::STANDARD.encode(buffer), }))) } diff --git a/tests/verify.rs b/tests/verify.rs index f33233137f..ac55c9aef3 100644 --- a/tests/verify.rs +++ b/tests/verify.rs @@ -6,7 +6,7 @@ fn verify() { CommandBuilder::new([ "verify", "--address", "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", - "--message", "Hello World", + "--text", "Hello World", "--witness", "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=" ]) .run_and_extract_stdout(), @@ -19,7 +19,7 @@ fn verify_fails() { CommandBuilder::new([ "verify", "--address", "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", - "--message", "Hello World - this should fail", + "--text", "Hello World - this should fail", "--witness", "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=" ]) .expected_exit_code(1) @@ -32,7 +32,7 @@ fn witness_and_transaction_conflict() { CommandBuilder::new([ "verify", "--address", "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", - "--message", "Hello World", + "--text", "Hello World", "--transaction", "asdf", "--witness", "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=" ]) @@ -55,7 +55,7 @@ fn verify_with_transaction() { "verify", "--address", "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", - "--message", + "--text", "Hello World", "--transaction", &tx, diff --git a/tests/wallet/sign.rs b/tests/wallet/sign.rs index d777213d82..01340521df 100644 --- a/tests/wallet/sign.rs +++ b/tests/wallet/sign.rs @@ -20,10 +20,10 @@ fn sign() { let address = addresses.first_key_value().unwrap().0; - let message = "HelloWorld"; + let text = "HelloWorld"; let sign = CommandBuilder::new(format!( - "wallet sign --address {} --message {message}", + "wallet sign --signer {} --text {text}", address.clone().assume_checked(), )) .core(&core) @@ -31,10 +31,9 @@ fn sign() { .run_and_deserialize_output::(); assert_eq!(address, &sign.address); - assert_eq!(message, &sign.message.unwrap()); CommandBuilder::new(format!( - "verify --address {} --message {message} --witness {}", + "verify --address {} --text {text} --witness {}", address.clone().assume_checked(), sign.witness, )) @@ -61,7 +60,7 @@ fn sign_file() { let address = addresses.first_key_value().unwrap().0; let sign = CommandBuilder::new(format!( - "wallet sign --address {} --file hello.txt", + "wallet sign --signer {} --file hello.txt", address.clone().assume_checked(), )) .write("hello.txt", "Hello World") @@ -70,7 +69,6 @@ fn sign_file() { .run_and_deserialize_output::(); assert_eq!(address, &sign.address); - assert!(sign.message.is_none()); CommandBuilder::new(format!( "verify --address {} --file hello.txt --witness {}", @@ -94,3 +92,57 @@ fn sign_file() { .stderr_regex("error: Invalid signature.*") .run_and_extract_stdout(); } + +#[test] +fn sign_for_inscription() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + let (inscription, _reveal) = inscribe(&core, &ord); + + core.mine_blocks(1); + + let addresses = CommandBuilder::new("wallet addresses") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::, Vec>>(); + + let text = "HelloWorld"; + + let sign = CommandBuilder::new(format!("wallet sign --signer {inscription} --text {text}",)) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert!(addresses.contains_key(&sign.address)); +} + +#[test] +fn sign_for_output() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let addresses = CommandBuilder::new("wallet addresses") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::, Vec>>(); + + let output = addresses.first_key_value().unwrap().1[0].output; + + let text = "HelloWorld"; + + let sign = CommandBuilder::new(format!("wallet sign --signer {output} --text {text}",)) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert!(addresses.contains_key(&sign.address)); +}