Skip to main content

kobe_svm/
derivation_style.rs

1//! Solana derivation path styles.
2//!
3//! Different Solana wallets use slightly different BIP-44 path layouts
4//! even though they all share SLIP-0010 Ed25519 as the underlying key
5//! scheme. This module captures the four widely-supported layouts and
6//! implements the chain-agnostic
7//! [`kobe_primitives::DerivationStyle`] trait so generic tooling (CLI
8//! rendering, property tests, agent helpers) can treat Solana the same
9//! way it treats EVM or TON.
10
11use alloc::format;
12use alloc::string::String;
13use core::fmt;
14use core::str::FromStr;
15
16use kobe_primitives::ParseDerivationStyleError;
17
18/// Solana derivation-path layouts, indexed by the account index.
19///
20/// # Path specifications
21///
22/// | Variant        | Path layout                     | Compatible wallets                        |
23/// | -------------- | ------------------------------- | ----------------------------------------- |
24/// | `Standard`     | `m/44'/501'/{index}'/0'`        | Phantom, Backpack, Solflare, Magic Eden   |
25/// | `Trust`        | `m/44'/501'/{index}'`           | Trust Wallet, Ledger (native), Keystone   |
26/// | `LedgerLive`   | `m/44'/501'/{index}'/0'/0'`     | Ledger Live                               |
27/// | `Legacy`       | `m/501'/{index}'/0'/0'`         | Older Phantom, Sollet (**deprecated**)    |
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
29#[non_exhaustive]
30pub enum DerivationStyle {
31    /// `m/44'/501'/{index}'/0'` — Phantom, Backpack, Solflare, …
32    #[default]
33    Standard,
34    /// `m/44'/501'/{index}'` — Trust Wallet, Ledger (native), Keystone.
35    Trust,
36    /// `m/44'/501'/{index}'/0'/0'` — Ledger Live.
37    LedgerLive,
38    /// `m/501'/{index}'/0'/0'` — legacy Phantom / Sollet (deprecated).
39    Legacy,
40}
41
42/// Every variant of [`DerivationStyle`], returned by
43/// [`kobe_primitives::DerivationStyle::all`].
44const ALL_STYLES: &[DerivationStyle] = &[
45    DerivationStyle::Standard,
46    DerivationStyle::Trust,
47    DerivationStyle::LedgerLive,
48    DerivationStyle::Legacy,
49];
50
51/// Tokens accepted by [`DerivationStyle::from_str`] (canonical + wallet aliases).
52const ACCEPTED_TOKENS: &[&str] = &[
53    "standard",
54    "phantom",
55    "backpack",
56    "solflare",
57    "trezor",
58    "trust",
59    "trustwallet",
60    "ledger",
61    "ledger-native",
62    "ledgernative",
63    "keystone",
64    "ledger-live",
65    "ledgerlive",
66    "live",
67    "legacy",
68    "old",
69    "sollet",
70];
71
72impl DerivationStyle {
73    /// Short machine-readable identifier (e.g. `"standard"`, `"ledger-live"`).
74    ///
75    /// Kept as an inherent `const fn` rather than a trait method because
76    /// it is Solana-specific API used by the CLI for backwards compatibility;
77    /// other chains do not all expose a short id.
78    #[must_use]
79    pub const fn id(self) -> &'static str {
80        match self {
81            Self::Standard => "standard",
82            Self::Trust => "trust",
83            Self::LedgerLive => "ledger-live",
84            Self::Legacy => "legacy",
85        }
86    }
87}
88
89impl kobe_primitives::DerivationStyle for DerivationStyle {
90    fn path(self, index: u32) -> String {
91        match self {
92            Self::Standard => format!("m/44'/501'/{index}'/0'"),
93            Self::Trust => format!("m/44'/501'/{index}'"),
94            Self::LedgerLive => format!("m/44'/501'/{index}'/0'/0'"),
95            Self::Legacy => format!("m/501'/{index}'/0'/0'"),
96        }
97    }
98
99    fn name(self) -> &'static str {
100        match self {
101            Self::Standard => "Standard (Phantom/Backpack)",
102            Self::Trust => "Trust (Ledger/Keystone)",
103            Self::LedgerLive => "Ledger Live",
104            Self::Legacy => "Legacy (deprecated)",
105        }
106    }
107
108    fn all() -> &'static [Self] {
109        ALL_STYLES
110    }
111}
112
113impl fmt::Display for DerivationStyle {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        f.write_str(<Self as kobe_primitives::DerivationStyle>::name(*self))
116    }
117}
118
119impl FromStr for DerivationStyle {
120    type Err = ParseDerivationStyleError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        match s.to_lowercase().as_str() {
124            "standard" | "phantom" | "backpack" | "solflare" | "trezor" => Ok(Self::Standard),
125            "trust" | "trustwallet" | "ledger" | "ledger-native" | "ledgernative" | "keystone" => {
126                Ok(Self::Trust)
127            }
128            "ledger-live" | "ledgerlive" | "live" => Ok(Self::LedgerLive),
129            "legacy" | "old" | "sollet" => Ok(Self::Legacy),
130            _ => Err(ParseDerivationStyleError::new("solana", s, ACCEPTED_TOKENS)),
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use kobe_primitives::DerivationStyle as _;
138
139    use super::*;
140
141    #[test]
142    fn test_standard_paths() {
143        let style = DerivationStyle::Standard;
144        assert_eq!(style.path(0), "m/44'/501'/0'/0'");
145        assert_eq!(style.path(1), "m/44'/501'/1'/0'");
146        assert_eq!(style.path(10), "m/44'/501'/10'/0'");
147    }
148
149    #[test]
150    fn test_trust_paths() {
151        let style = DerivationStyle::Trust;
152        assert_eq!(style.path(0), "m/44'/501'/0'");
153        assert_eq!(style.path(1), "m/44'/501'/1'");
154        assert_eq!(style.path(10), "m/44'/501'/10'");
155    }
156
157    #[test]
158    fn test_ledger_live_paths() {
159        let style = DerivationStyle::LedgerLive;
160        assert_eq!(style.path(0), "m/44'/501'/0'/0'/0'");
161        assert_eq!(style.path(1), "m/44'/501'/1'/0'/0'");
162        assert_eq!(style.path(10), "m/44'/501'/10'/0'/0'");
163    }
164
165    #[test]
166    fn test_legacy_paths() {
167        let style = DerivationStyle::Legacy;
168        assert_eq!(style.path(0), "m/501'/0'/0'/0'");
169        assert_eq!(style.path(1), "m/501'/1'/0'/0'");
170        assert_eq!(style.path(10), "m/501'/10'/0'/0'");
171    }
172
173    #[test]
174    fn test_from_str() {
175        // Standard aliases
176        assert_eq!(
177            "standard".parse::<DerivationStyle>().unwrap(),
178            DerivationStyle::Standard
179        );
180        assert_eq!(
181            "phantom".parse::<DerivationStyle>().unwrap(),
182            DerivationStyle::Standard
183        );
184        assert_eq!(
185            "backpack".parse::<DerivationStyle>().unwrap(),
186            DerivationStyle::Standard
187        );
188
189        // Trust aliases
190        assert_eq!(
191            "trust".parse::<DerivationStyle>().unwrap(),
192            DerivationStyle::Trust
193        );
194        assert_eq!(
195            "ledger".parse::<DerivationStyle>().unwrap(),
196            DerivationStyle::Trust
197        );
198        assert_eq!(
199            "keystone".parse::<DerivationStyle>().unwrap(),
200            DerivationStyle::Trust
201        );
202
203        // Ledger Live
204        assert_eq!(
205            "ledger-live".parse::<DerivationStyle>().unwrap(),
206            DerivationStyle::LedgerLive
207        );
208
209        // Legacy
210        assert_eq!(
211            "legacy".parse::<DerivationStyle>().unwrap(),
212            DerivationStyle::Legacy
213        );
214    }
215
216    #[test]
217    fn test_from_str_invalid() {
218        assert!("invalid".parse::<DerivationStyle>().is_err());
219    }
220
221    #[test]
222    fn test_default() {
223        assert_eq!(DerivationStyle::default(), DerivationStyle::Standard);
224    }
225}