use crate::account;
use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand};
use fuel_crypto::{Message, SecretKey, Signature};
use fuel_types::Bytes32;
use rpassword::prompt_password;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Debug, Args)]
pub struct Sign {
#[clap(long, value_name = "ACCOUNT_INDEX")]
pub account: Option<usize>,
#[clap(long)]
pub private_key: bool,
#[clap(long)]
pub private_key_non_interactive: Option<SecretKey>,
#[clap(long)]
pub password_non_interactive: Option<String>,
#[clap(subcommand)]
pub data: Data,
}
#[derive(Debug, Subcommand)]
pub enum Data {
TxId { tx_id: fuel_types::Bytes32 },
File { path: PathBuf },
String { string: String },
Hex { hex_string: String },
}
pub fn cli(wallet_path: &Path, sign: Sign) -> Result<()> {
let Sign {
account,
private_key,
private_key_non_interactive,
password_non_interactive,
data,
} = sign;
match (
account,
password_non_interactive,
private_key,
private_key_non_interactive,
) {
(Some(acc_ix), None, false, None) => wallet_account_cli(wallet_path, acc_ix, data)?,
(Some(acc_ix), Some(pw), false, None) => {
let msg = msg_from_data(data)?;
let sig = sign_msg_with_wallet_account(wallet_path, acc_ix, &msg, &pw)?;
println!("Signature: {sig}");
}
(None, None, _, Some(priv_key)) => {
let msg = msg_from_data(data)?;
let sig = Signature::sign(&priv_key, &msg);
println!("Signature: {sig}");
}
(None, None, true, None) => private_key_cli(data)?,
_ => bail!(
"Unexpected set of options passed to `forc wallet sign`.\n \
To sign with a wallet account, use `forc wallet sign --account <index> <data>`\n \
To sign with a private key, use `forc wallet sign --private <data>`",
),
}
Ok(())
}
pub(crate) fn wallet_account_cli(wallet_path: &Path, account_ix: usize, data: Data) -> Result<()> {
let msg = msg_from_data(data)?;
sign_msg_with_wallet_account_cli(wallet_path, account_ix, &msg)
}
pub(crate) fn private_key_cli(data: Data) -> Result<()> {
sign_msg_with_private_key_cli(&msg_from_data(data)?)
}
fn sign_msg_with_private_key_cli(msg: &Message) -> Result<()> {
let secret_key_input = prompt_password("Please enter the private key you wish to sign with: ")?;
let signature = sign_with_private_key_str(msg, &secret_key_input)?;
println!("Signature: {signature}");
Ok(())
}
fn sign_with_private_key_str(msg: &Message, priv_key_input: &str) -> Result<Signature> {
let secret_key = SecretKey::from_str(priv_key_input)?;
Ok(Signature::sign(&secret_key, msg))
}
fn sign_msg_with_wallet_account_cli(
wallet_path: &Path,
account_ix: usize,
msg: &Message,
) -> Result<()> {
let password = prompt_password("Please enter your wallet password: ")?;
let signature = sign_msg_with_wallet_account(wallet_path, account_ix, msg, &password)?;
println!("Signature: {signature}");
Ok(())
}
fn sign_msg_with_wallet_account(
wallet_path: &Path,
account_ix: usize,
msg: &Message,
pw: &str,
) -> Result<Signature> {
let secret_key = account::derive_secret_key(wallet_path, account_ix, pw)?;
Ok(Signature::sign(&secret_key, msg))
}
fn msg_from_hash32(hash: Bytes32) -> Message {
Message::from_bytes(hash.into())
}
fn msg_from_file(path: &Path) -> Result<Message> {
let bytes = std::fs::read(path).context("failed to read bytes from path")?;
Ok(Message::new(bytes))
}
fn msg_from_hex_str(hex_str: &str) -> Result<Message> {
let bytes = bytes_from_hex_str(hex_str)?;
Ok(Message::new(bytes))
}
fn msg_from_data(data: Data) -> Result<Message> {
let msg = match data {
Data::TxId { tx_id } => msg_from_hash32(tx_id),
Data::File { path } => msg_from_file(&path)?,
Data::Hex { hex_string } => msg_from_hex_str(&hex_string)?,
Data::String { string } => Message::new(string),
};
Ok(msg)
}
fn bytes_from_hex_str(mut hex_str: &str) -> Result<Vec<u8>> {
const PREFIX: &str = "0x";
if hex_str.starts_with(PREFIX) {
hex_str = &hex_str[PREFIX.len()..];
} else {
bail!("missing 0x at the beginning of hex string")
}
hex::decode(hex_str).context("failed to decode bytes from hex string")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::test_utils::{with_tmp_dir_and_wallet, TEST_PASSWORD};
use fuel_crypto::Message;
#[test]
fn sign_tx_id() {
with_tmp_dir_and_wallet(|_dir, wallet_path| {
let tx_id = Bytes32::from_str(
"0x6c226b276bd2028c0582229b6396f91801c913973487491b0262c5c7b3cd6e39",
)
.unwrap();
let msg = msg_from_hash32(tx_id);
let account_ix = 0;
let sig =
sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
assert_eq!(sig.to_string(), "bcf4651f072130aaf8925610e1d719b76e25b19b0a86779d3f4294964f1607cc95eb6c58eb37bf0510f618bd284decdf936c48ec6722df5472084e4098d54620");
});
}
const TEST_STR: &str = "Blah blah blah";
const EXPECTED_SIG: &str = "b0b2f29b52d95c1cba47ea7c7edeec6c84a0bd196df489e219f6f388b69d760479b994f4bae2d5f2abef7d5faf7d9f5ee3ea47ada4d15b7a7ee2777dcd7b36bb";
#[test]
fn sign_string() {
with_tmp_dir_and_wallet(|_dir, wallet_path| {
let msg = Message::new(TEST_STR);
let account_ix = 0;
let sig =
sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
assert_eq!(sig.to_string(), EXPECTED_SIG);
});
}
#[test]
fn sign_file() {
with_tmp_dir_and_wallet(|dir, wallet_path| {
let path = dir.join("data");
std::fs::write(&path, TEST_STR).unwrap();
let msg = msg_from_file(&path).unwrap();
let account_ix = 0;
let sig =
sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
assert_eq!(sig.to_string(), EXPECTED_SIG);
});
}
#[test]
fn sign_hex() {
with_tmp_dir_and_wallet(|_dir, wallet_path| {
let hex_encoded = format!("0x{}", hex::encode(TEST_STR));
let msg = msg_from_hex_str(&hex_encoded).unwrap();
let account_ix = 0;
let sig =
sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
assert_eq!(sig.to_string(), EXPECTED_SIG);
});
}
}