1use crate::account;
2use anyhow::{bail, Context, Result};
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::{with_tmp_dir_and_wallet, TEST_PASSWORD};
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!(sig.to_string(), "bcf4651f072130aaf8925610e1d719b76e25b19b0a86779d3f4294964f1607cc95eb6c58eb37bf0510f618bd284decdf936c48ec6722df5472084e4098d54620");
204 });
205 }
206
207 const TEST_STR: &str = "Blah blah blah";
208 const EXPECTED_SIG: &str = "b0b2f29b52d95c1cba47ea7c7edeec6c84a0bd196df489e219f6f388b69d760479b994f4bae2d5f2abef7d5faf7d9f5ee3ea47ada4d15b7a7ee2777dcd7b36bb";
209
210 #[test]
211 fn sign_string() {
212 with_tmp_dir_and_wallet(|_dir, wallet_path| {
213 let msg = Message::new(TEST_STR);
214 let account_ix = 0;
215 let sig =
216 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
217 assert_eq!(sig.to_string(), EXPECTED_SIG);
218 });
219 }
220
221 #[test]
222 fn sign_file() {
223 with_tmp_dir_and_wallet(|dir, wallet_path| {
224 let path = dir.join("data");
225 std::fs::write(&path, TEST_STR).unwrap();
226 let msg = msg_from_file(&path).unwrap();
227 let account_ix = 0;
228 let sig =
229 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
230 assert_eq!(sig.to_string(), EXPECTED_SIG);
231 });
232 }
233
234 #[test]
235 fn sign_hex() {
236 with_tmp_dir_and_wallet(|_dir, wallet_path| {
237 let hex_encoded = format!("0x{}", hex::encode(TEST_STR));
238 let msg = msg_from_hex_str(&hex_encoded).unwrap();
239 let account_ix = 0;
240 let sig =
241 sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
242 assert_eq!(sig.to_string(), EXPECTED_SIG);
243 });
244 }
245}