Ledger hardware wallet integration (#8068)
* Initial remote wallet module * Add clap derivation tooling * Add remote-wallet path apis * Implement remote-wallet in solana-keygen * Implement remote-wallet in cli for read-only pubkey usage * Linux: Use udev backend; add udev rules tool * Ignore Ledger live test * Cli api adjustments
This commit is contained in:
@ -34,6 +34,7 @@ solana-config-program = { path = "../programs/config", version = "0.24.0" }
|
||||
solana-faucet = { path = "../faucet", version = "0.24.0" }
|
||||
solana-logger = { path = "../logger", version = "0.24.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.24.0" }
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "0.24.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.24.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.24.0" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "0.24.0" }
|
||||
|
@ -20,6 +20,10 @@ use solana_client::{client_error::ClientError, rpc_client::RpcClient};
|
||||
use solana_faucet::faucet::request_airdrop_transaction;
|
||||
#[cfg(test)]
|
||||
use solana_faucet::faucet_mock::request_airdrop_transaction;
|
||||
use solana_remote_wallet::{
|
||||
ledger::get_ledger_from_info,
|
||||
remote_wallet::{DerivationPath, RemoteWallet, RemoteWalletInfo},
|
||||
};
|
||||
use solana_sdk::{
|
||||
bpf_loader,
|
||||
clock::{Epoch, Slot},
|
||||
@ -439,6 +443,7 @@ pub struct CliConfig {
|
||||
pub json_rpc_url: String,
|
||||
pub keypair: Keypair,
|
||||
pub keypair_path: Option<String>,
|
||||
pub derivation_path: Option<DerivationPath>,
|
||||
pub rpc_client: Option<RpcClient>,
|
||||
pub verbose: bool,
|
||||
}
|
||||
@ -453,6 +458,22 @@ impl CliConfig {
|
||||
pub fn default_json_rpc_url() -> String {
|
||||
"http://127.0.0.1:8899".to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn pubkey(&self) -> Result<Pubkey, Box<dyn std::error::Error>> {
|
||||
if let Some(path) = &self.keypair_path {
|
||||
if path.starts_with("usb://") {
|
||||
let (remote_wallet_info, mut derivation_path) =
|
||||
RemoteWalletInfo::parse_path(path.to_string())?;
|
||||
if let Some(derivation) = &self.derivation_path {
|
||||
let derivation = derivation.clone();
|
||||
derivation_path = derivation;
|
||||
}
|
||||
let ledger = get_ledger_from_info(remote_wallet_info)?;
|
||||
return Ok(ledger.get_pubkey(derivation_path)?);
|
||||
}
|
||||
}
|
||||
Ok(self.keypair.pubkey())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CliConfig {
|
||||
@ -465,6 +486,7 @@ impl Default for CliConfig {
|
||||
json_rpc_url: Self::default_json_rpc_url(),
|
||||
keypair: Keypair::new(),
|
||||
keypair_path: Some(Self::default_keypair_path()),
|
||||
derivation_path: None,
|
||||
rpc_client: None,
|
||||
verbose: false,
|
||||
}
|
||||
@ -845,7 +867,7 @@ fn process_create_address_with_seed(
|
||||
seed: &str,
|
||||
program_id: &Pubkey,
|
||||
) -> ProcessResult {
|
||||
let config_pubkey = config.keypair.pubkey();
|
||||
let config_pubkey = config.pubkey()?;
|
||||
let from_pubkey = from_pubkey.unwrap_or(&config_pubkey);
|
||||
let address = create_address_with_seed(from_pubkey, seed, program_id)?;
|
||||
Ok(address.to_string())
|
||||
@ -858,12 +880,13 @@ fn process_airdrop(
|
||||
lamports: u64,
|
||||
use_lamports_unit: bool,
|
||||
) -> ProcessResult {
|
||||
let pubkey = config.pubkey()?;
|
||||
println!(
|
||||
"Requesting airdrop of {} from {}",
|
||||
build_balance_message(lamports, use_lamports_unit, true),
|
||||
faucet_addr
|
||||
);
|
||||
let previous_balance = match rpc_client.retry_get_balance(&config.keypair.pubkey(), 5)? {
|
||||
let previous_balance = match rpc_client.retry_get_balance(&pubkey, 5)? {
|
||||
Some(lamports) => lamports,
|
||||
None => {
|
||||
return Err(CliError::RpcRequestError(
|
||||
@ -873,10 +896,10 @@ fn process_airdrop(
|
||||
}
|
||||
};
|
||||
|
||||
request_and_confirm_airdrop(&rpc_client, faucet_addr, &config.keypair.pubkey(), lamports)?;
|
||||
request_and_confirm_airdrop(&rpc_client, faucet_addr, &pubkey, lamports)?;
|
||||
|
||||
let current_balance = rpc_client
|
||||
.retry_get_balance(&config.keypair.pubkey(), 5)?
|
||||
.retry_get_balance(&pubkey, 5)?
|
||||
.unwrap_or(previous_balance);
|
||||
|
||||
Ok(build_balance_message(
|
||||
@ -892,7 +915,7 @@ fn process_balance(
|
||||
pubkey: &Option<Pubkey>,
|
||||
use_lamports_unit: bool,
|
||||
) -> ProcessResult {
|
||||
let pubkey = pubkey.unwrap_or(config.keypair.pubkey());
|
||||
let pubkey = pubkey.unwrap_or(config.pubkey()?);
|
||||
let balance = rpc_client.retry_get_balance(&pubkey, 5)?;
|
||||
match balance {
|
||||
Some(lamports) => Ok(build_balance_message(lamports, use_lamports_unit, true)),
|
||||
@ -1260,6 +1283,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||
println_name_value("RPC URL:", &config.json_rpc_url);
|
||||
if let Some(keypair_path) = &config.keypair_path {
|
||||
println_name_value("Keypair Path:", keypair_path);
|
||||
if keypair_path.starts_with("usb://") {
|
||||
println_name_value("Pubkey:", &format!("{:?}", config.pubkey()?));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1275,7 +1301,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||
match &config.command {
|
||||
// Cluster Query Commands
|
||||
// Get address of this client
|
||||
CliCommand::Address => Ok(format!("{}", config.keypair.pubkey())),
|
||||
CliCommand::Address => Ok(format!("{}", config.pubkey()?)),
|
||||
|
||||
// Return software version of solana-cli and cluster entrypoint node
|
||||
CliCommand::Catchup { node_pubkey } => process_catchup(&rpc_client, node_pubkey),
|
||||
|
@ -2,7 +2,8 @@ use clap::{crate_description, crate_name, AppSettings, Arg, ArgGroup, ArgMatches
|
||||
use console::style;
|
||||
|
||||
use solana_clap_utils::{
|
||||
input_validators::is_url,
|
||||
input_parsers::derivation_of,
|
||||
input_validators::{is_derivation, is_url},
|
||||
keypair::{
|
||||
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
|
||||
SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
@ -25,7 +26,7 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
|
||||
let config = Config::load(config_file).unwrap_or_default();
|
||||
if let Some(field) = subcommand_matches.value_of("specific_setting") {
|
||||
let (field_name, value, default_value) = match field {
|
||||
"url" => ("RPC Url", config.url, CliConfig::default_json_rpc_url()),
|
||||
"url" => ("RPC URL", config.url, CliConfig::default_json_rpc_url()),
|
||||
"keypair" => (
|
||||
"Key Path",
|
||||
config.keypair_path,
|
||||
@ -37,12 +38,12 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
|
||||
} else {
|
||||
println_name_value("Config File:", config_file);
|
||||
println_name_value_or(
|
||||
"RPC Url:",
|
||||
"RPC URL:",
|
||||
&config.url,
|
||||
&CliConfig::default_json_rpc_url(),
|
||||
);
|
||||
println_name_value_or(
|
||||
"Key Path:",
|
||||
"Keypair Path:",
|
||||
&config.keypair_path,
|
||||
&CliConfig::default_keypair_path(),
|
||||
);
|
||||
@ -106,7 +107,7 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
|
||||
let (keypair, keypair_path) = if require_keypair {
|
||||
let KeypairWithSource { keypair, source } = keypair_input(&matches, "keypair")?;
|
||||
match source {
|
||||
keypair::Source::File => (
|
||||
keypair::Source::Path => (
|
||||
keypair,
|
||||
Some(matches.value_of("keypair").unwrap().to_string()),
|
||||
),
|
||||
@ -126,12 +127,16 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
|
||||
default_keypair_path
|
||||
};
|
||||
|
||||
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
|
||||
Err(CliError::BadParameter(format!(
|
||||
"{}: Unable to open keypair file: {}",
|
||||
err, keypair_path
|
||||
)))
|
||||
})?;
|
||||
let keypair = if keypair_path.starts_with("usb://") {
|
||||
keypair
|
||||
} else {
|
||||
read_keypair_file(&keypair_path).or_else(|err| {
|
||||
Err(CliError::BadParameter(format!(
|
||||
"{}: Unable to open keypair file: {}",
|
||||
err, keypair_path
|
||||
)))
|
||||
})?
|
||||
};
|
||||
|
||||
(keypair, Some(keypair_path))
|
||||
}
|
||||
@ -146,6 +151,7 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
|
||||
json_rpc_url,
|
||||
keypair,
|
||||
keypair_path,
|
||||
derivation_path: derivation_of(matches, "derivation_path"),
|
||||
rpc_client: None,
|
||||
verbose: matches.is_present("verbose"),
|
||||
})
|
||||
@ -189,7 +195,15 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
.value_name("PATH")
|
||||
.global(true)
|
||||
.takes_value(true)
|
||||
.help("/path/to/id.json"),
|
||||
.help("/path/to/id.json or usb://remote/wallet/path"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("derivation_path")
|
||||
.long("derivation-path")
|
||||
.value_name("ACCOUNT or ACCOUNT/CHANGE")
|
||||
.takes_value(true)
|
||||
.validator(is_derivation)
|
||||
.help("Derivation path to use: m/44'/501'/ACCOUNT'/CHANGE'; default key is device base pubkey: m/44'/501'/0'")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("verbose")
|
||||
|
Reference in New Issue
Block a user