gmsol/cli/
wallet.rs

1use std::{collections::HashMap, path::PathBuf, rc::Rc};
2
3use eyre::OptionExt;
4use solana_remote_wallet::remote_wallet::RemoteWalletManager;
5use solana_sdk::signature::{read_keypair_file, Keypair};
6use url::Url;
7
8/// Parse url or path.
9fn parse_url_or_path(source: &str) -> eyre::Result<Url> {
10    let url = match Url::parse(source) {
11        Ok(url) => url,
12        Err(_) => {
13            let path = shellexpand::tilde(source);
14            let path: PathBuf = path.parse()?;
15            let path = std::fs::canonicalize(path)?;
16            Url::from_file_path(&path).expect("must be valid file path")
17        }
18    };
19
20    Ok(url)
21}
22
23/// Load keypair.
24pub fn load_keypair(source: &str) -> eyre::Result<Keypair> {
25    let url = parse_url_or_path(source)?;
26
27    match url.scheme() {
28        "file" => read_keypair_file(url.path()).map_err(|err| eyre::eyre!("{err}")),
29        other => {
30            eyre::bail!("{other} scheme is not support");
31        }
32    }
33}
34
35/// Load signer from url.
36pub fn signer_from_source(
37    source: &str,
38    confirm_key: bool,
39    keypair_name: &str,
40    wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
41) -> eyre::Result<crate::utils::LocalSignerRef> {
42    const QUERY_KEY: &str = "key";
43
44    use crate::utils::local_signer;
45    use anchor_client::solana_sdk::derivation_path::DerivationPath;
46    use solana_remote_wallet::{
47        locator::Locator, remote_keypair::generate_remote_keypair,
48        remote_wallet::maybe_wallet_manager,
49    };
50
51    let url = parse_url_or_path(source)?;
52
53    match url.scheme() {
54        "file" => {
55            let keypair = read_keypair_file(url.path()).map_err(|err| eyre::eyre!("{err}"))?;
56            Ok(local_signer(keypair))
57        }
58        "usb" => {
59            let manufacturer = url.host_str().ok_or_eyre("missing manufacturer")?;
60            let path = url.path();
61            let path = path.strip_prefix('/').unwrap_or(path);
62            let pubkey = (!path.is_empty()).then_some(path);
63            let locator = Locator::new_from_parts(manufacturer, pubkey)?;
64            let query = url.query_pairs().collect::<HashMap<_, _>>();
65            if query.len() > 1 {
66                eyre::bail!("invalid query string, extra fields not supported");
67            }
68            let derivation_path = query
69                .get(QUERY_KEY)
70                .map(|value| DerivationPath::from_key_str(value))
71                .transpose()?;
72            if wallet_manager.is_none() {
73                *wallet_manager = maybe_wallet_manager()?;
74            }
75            let wallet_manager = wallet_manager.as_ref().ok_or_eyre("no device found")?;
76            let keypair = generate_remote_keypair(
77                locator,
78                derivation_path.unwrap_or_default(),
79                wallet_manager,
80                confirm_key,
81                keypair_name,
82            )?;
83            Ok(local_signer(keypair))
84        }
85        scheme => Err(eyre::eyre!("unsupported scheme: {scheme}")),
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_parse_url_or_path() -> eyre::Result<()> {
95        let path = "~/.config/solana/id.json";
96        assert!(parse_url_or_path(path).is_ok());
97        Ok(())
98    }
99}