1use crate::account;
2use anyhow::{Context, Result, bail};
3use clap::{Args, Subcommand};
4use fuels::crypto::{Message, SecretKey, Signature};
5use fuels::types::Bytes32;
6use rpassword::prompt_password;
7use std::{
8 path::{Path, PathBuf},
9 str::FromStr,
10};
11
12#[derive(Debug, Args)]
15pub struct Sign {
16 #[clap(long, value_name = "ACCOUNT_INDEX")]
19 pub account: Option<usize>,
20 #[clap(long)]
23 pub private_key: bool,
24 #[clap(long)]
29 pub private_key_non_interactive: Option<SecretKey>,
30 #[clap(long)]
35 pub password_non_interactive: Option<String>,
36 #[clap(subcommand)]
37 pub data: Data,
38}
39
40#[derive(Debug, Subcommand)]
42pub enum Data {
43 TxId { tx_id: Bytes32 },
49 File { path: PathBuf },
51 String { string: String },
53 Hex { hex_string: String },
61}
62
63pub fn cli(ctx: &crate::CliContext, sign: Sign) -> Result<()> {
64 let Sign {
65 account,
66 private_key,
67 private_key_non_interactive,
68 password_non_interactive,
69 data,
70 } = sign;
71 match (
72 account,
73 password_non_interactive,
74 private_key,
75 private_key_non_interactive,
76 ) {
77 (Some(acc_ix), None, false, None) => wallet_account_cli(ctx, acc_ix, data)?,
79 (Some(acc_ix), Some(pw), false, None) => {
81 let msg = msg_from_data(data)?;
82 let sig = sign_msg_with_wallet_account(&ctx.wallet_path, acc_ix, &msg, &pw)?;
83 println!("Signature: {sig}");
84 }
85 (None, None, _, Some(priv_key)) => {
87 let msg = msg_from_data(data)?;
88 let sig = Signature::sign(&priv_key, &msg);
89 println!("Signature: {sig}");
90 }
91 (None, None, true, None) => private_key_cli(data)?,
93 _ => bail!(
95 "Unexpected set of options passed to `forc wallet sign`.\n \
96 To sign with a wallet account, use `forc wallet sign --account <index> <data>`\n \
97 To sign with a private key, use `forc wallet sign --private <data>`",
98 ),
99 }
100 Ok(())
101}
102
103pub(crate) fn wallet_account_cli(
104 ctx: &crate::CliContext,
105 account_ix: usize,
106 data: Data,
107) -> Result<()> {
108 let msg = msg_from_data(data)?;
109 sign_msg_with_wallet_account_cli(&ctx.wallet_path, account_ix, &msg)
110}
111
112pub(crate) fn private_key_cli(data: Data) -> Result<()> {
113 sign_msg_with_private_key_cli(&msg_from_data(data)?)
114}
115
116fn sign_msg_with_private_key_cli(msg: &Message) -> Result<()> {
117 let secret_key_input = prompt_password("Please enter the private key you wish to sign with: ")?;
118 let signature = sign_with_private_key_str(msg, &secret_key_input)?;
119 println!("Signature: {signature}");
120 Ok(())
121}
122
123fn sign_with_private_key_str(msg: &Message, priv_key_input: &str) -> Result<Signature> {
124 let secret_key = SecretKey::from_str(priv_key_input)?;
125 Ok(Signature::sign(&secret_key, msg))
126}
127
128fn sign_msg_with_wallet_account_cli(
129 wallet_path: &Path,
130 account_ix: usize,
131 msg: &Message,
132) -> Result<()> {
133 let password = prompt_password("Please enter your wallet password: ")?;
134 let signature = sign_msg_with_wallet_account(wallet_path, account_ix, msg, &password)?;
135 println!("Signature: {signature}");
136 Ok(())
137}
138
139fn sign_msg_with_wallet_account(
140 wallet_path: &Path,
141 account_ix: usize,
142 msg: &Message,
143 pw: &str,
144) -> Result<Signature> {
145 let secret_key = account::derive_secret_key(wallet_path, account_ix, pw)?;
146 Ok(Signature::sign(&secret_key, msg))
147}
148
149fn msg_from_hash32(hash: Bytes32) -> Message {
152 Message::from_bytes(hash.into())
153}
154
155fn msg_from_file(path: &Path) -> Result<Message> {
156 let bytes = std::fs::read(path).context("failed to read bytes from path")?;
157 Ok(Message::new(bytes))
158}
159
160fn msg_from_hex_str(hex_str: &str) -> Result<Message> {
161 let bytes = bytes_from_hex_str(hex_str)?;
162 Ok(Message::new(bytes))
163}
164
165fn msg_from_data(data: Data) -> Result<Message> {
166 let msg = match data {
167 Data::TxId { tx_id } => msg_from_hash32(tx_id),
168 Data::File { path } => msg_from_file(&path)?,
169 Data::Hex { hex_string } => msg_from_hex_str(&hex_string)?,
170 Data::String { string } => Message::new(string),
171 };
172 Ok(msg)
173}
174
175fn bytes_from_hex_str(mut hex_str: &str) -> Result<Vec<u8>> {
176 const PREFIX: &str = "0x";
178 if hex_str.starts_with(PREFIX) {
179 hex_str = &hex_str[PREFIX.len()..];
180 } else {
181 bail!("missing 0x at the beginning of hex string")
182 }
183 hex::decode(hex_str).context("failed to decode bytes from hex string")
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::utils::test_utils::{TEST_PASSWORD, with_tmp_dir_and_wallet};
190 use fuels::crypto::Message;
191
192 #[test]
193 fn sign_tx_id() {
194 with_tmp_dir_and_wallet(|_dir, wallet_path| {
195 let tx_id = Bytes32::from_str(
196 "0x6c226b276bd2028c0582229b6396f91801c913973487491b0262c5c7b3cd6e39",
197 )
198 .unwrap();
199 let msg = msg_from_hash32(tx_id);
200 let account_ix = 0;
201 let sig =
202 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
203 assert_eq!(
204 sig.to_string(),
205 "bcf4651f072130aaf8925610e1d719b76e25b19b0a86779d3f4294964f1607cc95eb6c58eb37bf0510f618bd284decdf936c48ec6722df5472084e4098d54620"
206 );
207 });
208 }
209
210 const TEST_STR: &str = "Blah blah blah";
211 const EXPECTED_SIG: &str = "b0b2f29b52d95c1cba47ea7c7edeec6c84a0bd196df489e219f6f388b69d760479b994f4bae2d5f2abef7d5faf7d9f5ee3ea47ada4d15b7a7ee2777dcd7b36bb";
212
213 #[test]
214 fn sign_string() {
215 with_tmp_dir_and_wallet(|_dir, wallet_path| {
216 let msg = Message::new(TEST_STR);
217 let account_ix = 0;
218 let sig =
219 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
220 assert_eq!(sig.to_string(), EXPECTED_SIG);
221 });
222 }
223
224 #[test]
225 fn sign_file() {
226 with_tmp_dir_and_wallet(|dir, wallet_path| {
227 let path = dir.join("data");
228 std::fs::write(&path, TEST_STR).unwrap();
229 let msg = msg_from_file(&path).unwrap();
230 let account_ix = 0;
231 let sig =
232 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
233 assert_eq!(sig.to_string(), EXPECTED_SIG);
234 });
235 }
236
237 #[test]
238 fn sign_hex() {
239 with_tmp_dir_and_wallet(|_dir, wallet_path| {
240 let hex_encoded = format!("0x{}", hex::encode(TEST_STR));
241 let msg = msg_from_hex_str(&hex_encoded).unwrap();
242 let account_ix = 0;
243 let sig =
244 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
245 assert_eq!(sig.to_string(), EXPECTED_SIG);
246 });
247 }
248}