Skip to main content

gmsol_cli/
wallet.rs

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