Skip to content

Commit

Permalink
feat: use Ledger as signer via --ledger-path
Browse files Browse the repository at this point in the history
Makes Ledger available as signer for any command expecting a signer by
simply setting the `--ledger-path` option with an EIP-2645 compliant
HD wallet path, or using the `STARKNET_LEDGER_PATH` environment
variable.
  • Loading branch information
xJonathanLEI committed Jun 29, 2024
1 parent 825adea commit 0c1627c
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl AccountArgs {
ExecutionEncoding::New,
)
} else {
let signer = signer.resolve()?;
let signer = signer.resolve().await?;
let account = PathBuf::from(shellexpand::tilde(&self.account).into_owned());

if !account.exists() {
Expand Down
124 changes: 104 additions & 20 deletions src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@ use clap::Parser;
use colored::Colorize;
use starknet::{
core::{crypto::Signature, types::Felt},
signers::{LocalWallet, Signer, SigningKey, VerifyingKey},
signers::{DerivationPath, LedgerSigner, LocalWallet, Signer, SigningKey, VerifyingKey},
};

#[derive(Debug)]
pub enum AnySigner {
LocalWallet(LocalWallet),
Ledger(LedgerSigner),
}

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum AnySignerGetPublicKeyError {
LocalWallet(<LocalWallet as Signer>::GetPublicKeyError),
Ledger(<LedgerSigner as Signer>::GetPublicKeyError),
}

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum AnySignerSignError {
LocalWallet(<LocalWallet as Signer>::SignError),
Ledger(<LedgerSigner as Signer>::SignError),
}

#[derive(Debug, Clone, Parser)]
Expand All @@ -37,6 +40,8 @@ pub struct SignerArgs {
keystore_password: Option<String>,
#[clap(long, help = private_key_help())]
private_key: Option<String>,
#[clap(long, help = ledger_path_help())]
ledger_path: Option<DerivationPath>,
}

#[derive(Debug)]
Expand All @@ -53,6 +58,7 @@ pub enum SignerResolutionTask {
pub enum SignerResolutionTaskContent {
Keystore(KeystoreTaskContent),
PrivateKey(PrivateKeyTaskContent),
Ledger(LedgerTaskContent),
}

#[derive(Debug)]
Expand All @@ -66,11 +72,21 @@ pub struct PrivateKeyTaskContent {
key: String,
}

#[derive(Debug)]
pub struct LedgerTaskContent {
path: DerivationPath,
}

enum StringValue {
FromCommandLine(String),
FromEnvVar(String),
}

enum DerivationPathValue {
FromCommandLine(DerivationPath),
FromEnvVar(DerivationPath),
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl Signer for AnySigner {
Expand All @@ -82,6 +98,9 @@ impl Signer for AnySigner {
Self::LocalWallet(inner) => Ok(<LocalWallet as Signer>::get_public_key(inner)
.await
.map_err(Self::GetPublicKeyError::LocalWallet)?),
Self::Ledger(inner) => Ok(<LedgerSigner as Signer>::get_public_key(inner)
.await
.map_err(Self::GetPublicKeyError::Ledger)?),
}
}

Expand All @@ -90,19 +109,26 @@ impl Signer for AnySigner {
Self::LocalWallet(inner) => Ok(<LocalWallet as Signer>::sign_hash(inner, hash)
.await
.map_err(Self::SignError::LocalWallet)?),
Self::Ledger(inner) => {
eprintln!("Please confirm the signing operation on your Ledger");
Ok(<LedgerSigner as Signer>::sign_hash(inner, hash)
.await
.map_err(Self::SignError::Ledger)?)
}
}
}

fn is_interactive(&self) -> bool {
match self {
Self::LocalWallet(inner) => <LocalWallet as Signer>::is_interactive(inner),
Self::Ledger(inner) => <LedgerSigner as Signer>::is_interactive(inner),
}
}
}

impl SignerArgs {
pub fn into_signer(self) -> Result<AnySigner> {
self.into_task()?.resolve()
pub async fn into_signer(self) -> Result<AnySigner> {
self.into_task()?.resolve().await
}

/// Parses the options into a resolution task without immediately performing the resolution.
Expand All @@ -129,17 +155,25 @@ impl SignerArgs {
Err(_) => None,
},
};
let ledger_path = match self.ledger_path {
Some(value) => Some(DerivationPathValue::FromCommandLine(value)),
None => match std::env::var("STARKNET_LEDGER_PATH") {
Ok(value) => Some(DerivationPathValue::FromEnvVar(value.parse()?)),
Err(_) => None,
},
};

let task = match (keystore, self.keystore_password, private_key) {
let task = match (keystore, self.keystore_password, private_key, ledger_path) {
// Options:
// Keystore: from command line
// Private key: from env var or not supplied at all
// Ledger path: from env var or not supplied at all
// Resolution: use keystore
(Some(StringValue::FromCommandLine(keystore)), keystore_password, None)
| (
(
Some(StringValue::FromCommandLine(keystore)),
keystore_password,
Some(StringValue::FromEnvVar(_)),
Some(StringValue::FromEnvVar(_)) | None,
Some(DerivationPathValue::FromEnvVar(_)) | None,
) => SignerResolutionTask::Strong(SignerResolutionTaskContent::Keystore(
KeystoreTaskContent {
keystore,
Expand All @@ -149,20 +183,35 @@ impl SignerArgs {
// Options:
// Keystore: from env var or not supplied at all
// Private key: from command line
// Ledger path: from env var or not supplied at all
// Resolution: use private key
(None, None, Some(StringValue::FromCommandLine(private_key)))
| (
Some(StringValue::FromEnvVar(_)),
(
Some(StringValue::FromEnvVar(_)) | None,
None,
Some(StringValue::FromCommandLine(private_key)),
Some(DerivationPathValue::FromEnvVar(_)) | None,
) => SignerResolutionTask::Strong(SignerResolutionTaskContent::PrivateKey(
PrivateKeyTaskContent { key: private_key },
)),
// Options:
// Keystore: from env var or not supplied at all
// Private key: from env var or not supplied at all
// Ledger path: from command line
// Resolution: use Ledger
(
Some(StringValue::FromEnvVar(_)) | None,
None,
Some(StringValue::FromEnvVar(_)) | None,
Some(DerivationPathValue::FromCommandLine(ledger_path)),
) => SignerResolutionTask::Strong(SignerResolutionTaskContent::Ledger(
LedgerTaskContent { path: ledger_path },
)),
// Options:
// Keystore: from env var
// Private key: not supplied at all
// Ledger path: not supplied at all
// Resolution: use keystore (weak)
(Some(StringValue::FromEnvVar(keystore)), keystore_password, None) => {
(Some(StringValue::FromEnvVar(keystore)), keystore_password, None, None) => {
SignerResolutionTask::Weak(SignerResolutionTaskContent::Keystore(
KeystoreTaskContent {
keystore,
Expand All @@ -173,22 +222,41 @@ impl SignerArgs {
// Options:
// Keystore: not supplied at all
// Private key: from env var
// Ledger path: not supplied at all
// Resolution: use private key (weak)
(None, None, Some(StringValue::FromEnvVar(private_key))) => SignerResolutionTask::Weak(
SignerResolutionTaskContent::PrivateKey(PrivateKeyTaskContent { key: private_key }),
),
(None, None, Some(StringValue::FromEnvVar(private_key)), None) => {
SignerResolutionTask::Weak(SignerResolutionTaskContent::PrivateKey(
PrivateKeyTaskContent { key: private_key },
))
}
// Options:
// Keystore: not supplied at all
// Private key: not supplied at all
// Ledger path: from env var
// Resolution: use Ledger (weak)
(None, None, None, Some(DerivationPathValue::FromEnvVar(ledger_path))) => {
SignerResolutionTask::Weak(SignerResolutionTaskContent::Ledger(LedgerTaskContent {
path: ledger_path,
}))
}
// Options:
// Keystore: from env var
// Private key: from env var
// Ledger path: from env var
// Resolution: conflict
// (We don't really need this branch, but it's nice to show a case-specific warning.)
(Some(StringValue::FromEnvVar(_)), _, Some(StringValue::FromEnvVar(_))) => {
(
Some(StringValue::FromEnvVar(_)),
_,
Some(StringValue::FromEnvVar(_)),
Some(DerivationPathValue::FromEnvVar(_)),
) => {
return Err(anyhow::anyhow!(
"using STARKNET_KEYSTORE and STARKNET_PRIVATE_KEY \
"using STARKNET_KEYSTORE, STARKNET_PRIVATE_KEY, STARKNET_LEDGER_PATH \
at the same time is not allowed"
))
}
(None, None, None) => SignerResolutionTask::None,
(None, None, None, None) => SignerResolutionTask::None,
_ => {
return Err(anyhow::anyhow!(
"invalid signer option combination. \
Expand All @@ -202,11 +270,12 @@ impl SignerArgs {
}

impl SignerResolutionTask {
pub fn resolve(self) -> Result<AnySigner> {
pub async fn resolve(self) -> Result<AnySigner> {
match self {
Self::Strong(task) | Self::Weak(task) => match task {
SignerResolutionTaskContent::Keystore(inner) => inner.resolve(),
SignerResolutionTaskContent::PrivateKey(inner) => inner.resolve(),
SignerResolutionTaskContent::Ledger(inner) => inner.resolve().await,
},
Self::None => Err(anyhow::anyhow!(
"no valid signer option provided. \
Expand Down Expand Up @@ -258,12 +327,12 @@ impl PrivateKeyTaskContent {
Err(_) => true,
};

// TODO: change to recommend hardware wallets when they become available
if print_warning {
eprintln!(
"{}",
"WARNING: using private key in plain text is highly insecure, and you should \
ONLY do this for development. Consider using an encrypted keystore instead. \
ONLY do this for development. Consider using an encrypted keystore instead, \
or better yet, Ledger hardware wallets. \
(Check out https://book.starkli.rs/signers on how to suppress this warning)"
.bright_magenta()
);
Expand All @@ -276,6 +345,12 @@ impl PrivateKeyTaskContent {
}
}

impl LedgerTaskContent {
pub async fn resolve(self) -> Result<AnySigner> {
Ok(AnySigner::Ledger(LedgerSigner::new(self.path).await?))
}
}

fn keystore_help() -> String {
format!(
"Path to keystore JSON file [env: STARKNET_KEYSTORE={}]",
Expand All @@ -289,3 +364,12 @@ fn private_key_help() -> String {
std::env::var("STARKNET_PRIVATE_KEY").unwrap_or_default()
)
}

fn ledger_path_help() -> String {
format!(
"For using Ledger hardware wallets, an HD wallet derivation path with EIP-2645 \
standard, such as \"m/2645'/1195502025'/1470455285'/0'/0'/0\" \
[env: STARKNET_LEDGER_PATH={}]",
std::env::var("STARKNET_LEDGER_PATH").unwrap_or_default()
)
}
2 changes: 1 addition & 1 deletion src/subcommands/account/argent/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl Init {
anyhow::bail!("account config file already exists");
}

let signer = self.signer.into_signer()?;
let signer = self.signer.into_signer().await?;

// Too lazy to write random salt generation
let salt = SigningKey::from_random().secret_scalar();
Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/account/braavos/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl Init {
anyhow::bail!("account config file already exists");
}

let signer = self.signer.into_signer()?;
let signer = self.signer.into_signer().await?;

// Too lazy to write random salt generation
let salt = SigningKey::from_random().secret_scalar();
Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/account/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl Deploy {
}

let provider = Arc::new(self.provider.into_provider()?);
let signer = Arc::new(self.signer.into_signer()?);
let signer = Arc::new(self.signer.into_signer().await?);

if !self.file.exists() {
anyhow::bail!("account config file not found");
Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/account/oz/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl Init {
anyhow::bail!("account config file already exists");
}

let signer = self.signer.into_signer()?;
let signer = self.signer.into_signer().await?;

// Too lazy to write random salt generation
let salt = SigningKey::from_random().secret_scalar();
Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/signer/ledger/get_public_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct GetPublicKey {
help = "Display the public key on Ledger's screen for extra security"
)]
display: bool,
#[clap(help = "A HD wallet derivation path with EIP-2645 standard, such as \
#[clap(help = "An HD wallet derivation path with EIP-2645 standard, such as \
\"m/2645'/1195502025'/1470455285'/0'/0'/0\"")]
path: DerivationPath,
}
Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/signer/ledger/sign_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use starknet::{
pub struct SignHash {
#[clap(
long,
help = "A HD wallet derivation path with EIP-2645 standard, such as \
help = "An HD wallet derivation path with EIP-2645 standard, such as \
\"m/2645'/1195502025'/1470455285'/0'/0'/0\""
)]
path: DerivationPath,
Expand Down

0 comments on commit 0c1627c

Please sign in to comment.