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
12fn 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
27pub 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
39pub 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}