rialo-cdk 0.2.0-alpha.0

Rialo CDK - A comprehensive toolkit for building with the Rialo blockchain
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Mnemonic-related functionality for wallet generation and key derivation.
//!
//! This module provides tools for generating BIP39 mnemonic phrases, deriving
//! Ed25519 keypairs from mnemonics using BIP32/BIP44 derivation paths, and
//! creating child keypairs for hierarchical deterministic wallets.

use bip32::{ChildNumber, DerivationPath};
use bip39::{Language, Mnemonic, MnemonicType, Seed};
use ed25519_dalek::SigningKey as Keypair;
use slip10_ed25519::derive_ed25519_private_key;

use crate::{
    constants::{BASE_DERIVATION_PATH, DERIVATION_PATH_COIN_TYPE, DERIVATION_PATH_PURPOSE_ED25519},
    error::{Result, RialoError},
};

/// Generates a new 12-word BIP39 mnemonic phrase.
///
/// Creates a cryptographically secure mnemonic phrase that can be used
/// as the foundation for a hierarchical deterministic wallet.
///
/// # Returns
///
/// Returns a `Result` containing the 12-word mnemonic phrase as a `String`
/// on success.
///
/// # Examples
///
/// ```
/// use rialo_cdk::wallet::mnemonic;
///
/// let mnemonic_phrase = mnemonic::generate_mnemonic().unwrap();
/// println!("Generated mnemonic: {}", mnemonic_phrase);
/// ```
#[cfg(feature = "mnemonic")]
pub fn generate_mnemonic() -> Result<String> {
    let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
    Ok(mnemonic.phrase().to_string())
}

/// Validates derivation path for Ed25519 (SLIP-0010)
fn validate_path(path: Option<DerivationPath>) -> Result<DerivationPath> {
    match path {
        Some(p) => {
            // Expect: m/{purpose}'/{coin_type}'/{account}'/0'
            if let &[purpose, coin_type, account, change] = p.as_ref() {
                if Some(purpose) == ChildNumber::new(DERIVATION_PATH_PURPOSE_ED25519, true).ok()
                    && Some(coin_type) == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok()
                    && account.is_hardened()
                    && change.is_hardened()
                {
                    Ok(p)
                } else {
                    Err(RialoError::Keyring(format!(
                        "Invalid path: must be m/{}'/{}'/<account>'/0' with all hardened",
                        DERIVATION_PATH_PURPOSE_ED25519, DERIVATION_PATH_COIN_TYPE
                    )))
                }
            } else {
                Err(RialoError::Keyring(
                    "Invalid path: expected 4 levels".to_string(),
                ))
            }
        }
        None => {
            // Default: m/{purpose}'/{coin_type}'/0'/0'
            format!("m/{DERIVATION_PATH_PURPOSE_ED25519}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0'")
                .parse()
                .map_err(|_| RialoError::Keyring("Cannot parse default path".to_string()))
        }
    }
}

/// Derives an Ed25519 keypair from a BIP39 mnemonic phrase using SLIP-0010.
///
/// This function implements hierarchical deterministic (HD) wallet derivation
/// following BIP32/BIP44 standards adapted for Ed25519 via SLIP-0010. It derives
/// a keypair at the path: `m/44'/501'/{account_index}'/0'`
///
/// # Arguments
///
/// * `mnemonic` - A valid BIP39 mnemonic phrase (12, 15, 18, 21, or 24 words)
/// * `passphrase` - Optional passphrase for additional security (BIP39 extension)
/// * `account_index` - The account index in the derivation path (hardened)
///
/// # Derivation Path
///
/// The function uses the Rialo derivation path:
/// - `m/44'/756'/{account_index}'/0'`
///   - `44'` - BIP44 purpose (hardened)
///   - `756'` - Rialo coin type (hardened)
///   - `{account_index}'` - Account number (hardened)
///   - `0'` - Change address (hardened)
///
/// All path components are hardened (marked with `'`) as required by SLIP-0010
/// for Ed25519 derivation.
///
/// # Returns
///
/// Returns a `Result` containing the derived Ed25519 `Keypair` on success.
///
/// # Errors
///
/// Returns `RialoError::Keyring` if:
/// - The mnemonic phrase is invalid or malformed
/// - The derivation path cannot be constructed
/// - Key derivation fails
///
/// # Examples
///
/// ```
/// use rialo_cdk::wallet::mnemonic;
///
/// // Derive the first account (index 0)
/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
/// let keypair = mnemonic::derive_keypair(mnemonic, None, 0).unwrap();
///
/// // Derive with passphrase for additional security
/// let keypair_protected = mnemonic::derive_keypair(
///     mnemonic,
///     Some("my secure passphrase"),
///     0
/// ).unwrap();
///
/// // Derive multiple accounts from same mnemonic
/// let account_0 = mnemonic::derive_keypair(mnemonic, None, 0).unwrap();
/// let account_1 = mnemonic::derive_keypair(mnemonic, None, 1).unwrap();
/// let account_2 = mnemonic::derive_keypair(mnemonic, None, 2).unwrap();
/// ```
///
/// # Security Considerations
///
/// - **Never share or expose your mnemonic phrase** - it provides complete access to all derived keys
/// - Store mnemonics securely offline (paper wallet, hardware wallet, secure vault)
/// - Consider using a passphrase for additional security (BIP39 extension)
/// - The same mnemonic + passphrase + account_index always produces the same keypair
/// - Different account indices produce cryptographically independent keypairs
///
/// # Compatibility
///
/// This implementation is compatible with:
/// - Solana CLI wallet derivation
/// - Phantom, Solflare, and other Solana wallets
/// - Any wallet following BIP44 with Solana coin type 501
#[cfg(all(feature = "mnemonic", feature = "hd-wallet"))]
pub fn derive_keypair(
    mnemonic: &str,
    passphrase: Option<&str>,
    account_index: u32,
) -> Result<Keypair> {
    // Parse mnemonic
    let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English)
        .map_err(|e| RialoError::Keyring(format!("Invalid mnemonic: {e}")))?;

    // Generate seed
    let passphrase = passphrase.unwrap_or("");
    let seed = Seed::new(&mnemonic, passphrase);

    // Build path: m/{purpose}'/{coin_type}'/{account}'/0'
    let path_str = format!("{}{}'/0'", BASE_DERIVATION_PATH, account_index);
    let path: DerivationPath = path_str
        .parse()
        .map_err(|_| RialoError::Keyring("Cannot parse path".to_string()))?;

    let validated_path = validate_path(Some(path))?;

    // Convert path to indexes
    // For SLIP-0010, hardened indexes need 0x80000000 bit set
    let indexes: Vec<u32> = validated_path
        .into_iter()
        .map(|child| {
            let index: u32 = child.into();
            if child.is_hardened() {
                index | 0x80000000
            } else {
                index
            }
        })
        .collect();

    let derived = derive_ed25519_private_key(seed.as_bytes(), &indexes);

    let keypair = Keypair::from_bytes(&derived);
    Ok(keypair)
}

/// Derives an Ed25519 keypair from a mnemonic using a custom derivation path.
///
/// This function provides fine-grained control over the derivation path, allowing
/// you to specify a custom BIP32 path string. The path must follow SLIP-0010
/// requirements for Ed25519: all components must be hardened.
///
/// # Arguments
///
/// * `mnemonic` - A valid BIP39 mnemonic phrase
/// * `passphrase` - Optional passphrase for additional security
/// * `path` - Custom derivation path string (e.g., `"m/44'/501'/0'/0'"`)
///
/// # Path Requirements
///
/// The derivation path must:
/// - Start with `m/` (master key)
/// - Have exactly 4 levels: `m/{purpose}'/{coin_type}'/{account}'/0'`
/// - Use `44'` for purpose (BIP44 standard)
/// - Use `501'` for coin type (Solana)
/// - Have all components hardened (marked with `'`)
///
/// # Returns
///
/// Returns a `Result` containing the derived Ed25519 `Keypair` on success.
///
/// # Errors
///
/// Returns `RialoError::Keyring` if:
/// - The mnemonic phrase is invalid
/// - The path string cannot be parsed
/// - The path doesn't meet SLIP-0010 requirements (wrong length, non-hardened components)
/// - The purpose is not 44' or coin type is not 756'
///
/// # Examples
///
/// ```
/// use rialo_cdk::wallet::mnemonic;
///
/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
///
/// // Derive with standard path
/// let keypair = mnemonic::derive_keypair_from_path(
///     mnemonic,
///     None,
///     "m/44'/756'/0'/0'"
/// ).unwrap();
///
/// // Derive account 5
/// let account_5 = mnemonic::derive_keypair_from_path(
///     mnemonic,
///     None,
///     "m/44'/756'/5'/0'"
/// ).unwrap();
///
/// // With passphrase
/// let protected = mnemonic::derive_keypair_from_path(
///     mnemonic,
///     Some("my passphrase"),
///     "m/44'/756'/0'/0'"
/// ).unwrap();
/// ```
///
/// # Invalid Paths
///
/// The following paths will fail validation:
/// - `"m/44/756/0/0"` - Components not hardened
/// - `"m/44'/756'/0'"` - Wrong number of levels (only 3)
/// - `"m/49'/756'/0'/0'"` - Wrong purpose (should be 44')
/// - `"m/44'/0'/0'/0'"` - Wrong coin type (should be 756')
///
/// # When to Use
///
/// Use this function when you need:
/// - Custom derivation paths for testing
/// - Compatibility with non-standard wallet implementations
/// - Explicit control over the derivation path
///
/// For standard use cases, prefer [`derive_keypair`] which handles path
/// construction automatically.
#[cfg(all(feature = "mnemonic", feature = "hd-wallet"))]
pub fn derive_keypair_from_path(
    mnemonic: &str,
    passphrase: Option<&str>,
    path: &str,
) -> Result<Keypair> {
    let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English)
        .map_err(|e| RialoError::Keyring(format!("Invalid mnemonic: {e}")))?;

    let passphrase = passphrase.unwrap_or("");
    let seed = Seed::new(&mnemonic, passphrase);

    let path: DerivationPath = path
        .parse()
        .map_err(|_| RialoError::Keyring(format!("Cannot parse path: {}", path)))?;
    let validated_path = validate_path(Some(path))?;

    // Convert path to indexes with hardened bit
    let indexes: Vec<u32> = validated_path
        .into_iter()
        .map(|child| {
            let index: u32 = child.into();
            if child.is_hardened() {
                index | 0x80000000
            } else {
                index
            }
        })
        .collect();
    let derived = derive_ed25519_private_key(seed.as_bytes(), &indexes);

    let keypair = Keypair::from_bytes(&derived);
    Ok(keypair)
}

/// Convenience function to derive the default keypair (account 0) from a mnemonic.
///
/// This is a convenience wrapper around [`derive_keypair`] that always uses account
/// index 0, which is the standard first account for most wallet implementations.
/// Use this when you need a simple, default keypair from a mnemonic phrase.
///
/// # Arguments
///
/// * `mnemonic` - A valid BIP39 mnemonic phrase (12, 15, 18, 21, or 24 words)
/// * `passphrase` - Optional passphrase for additional security (BIP39 extension)
///
/// # Derivation Path
///
/// Always derives at: `m/44'/756'/0'/0'` (Rialo's first account)
///
/// # Returns
///
/// Returns a `Result` containing the derived Ed25519 `Keypair` for account 0.
///
/// # Errors
///
/// Returns `RialoError::Keyring` if:
/// - The mnemonic phrase is invalid or malformed
/// - Key derivation fails
///
/// # Examples
///
/// ```
/// use rialo_cdk::wallet::mnemonic;
///
/// // Derive default account without passphrase
/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
/// let keypair = mnemonic::keypair_from_mnemonic(mnemonic, None).unwrap();
///
/// // Derive default account with passphrase protection
/// let protected = mnemonic::keypair_from_mnemonic(
///     mnemonic,
///     Some("my secure passphrase")
/// ).unwrap();
/// ```
///
/// # When to Use
///
/// - **Use this** for simple wallet setups with a single account
/// - **Use this** when you don't need multiple accounts from the same mnemonic
/// - **Use [`derive_keypair`]** when you need specific account indices
/// - **Use [`derive_child_keypair`]** when you need multiple accounts without passphrases
///
/// # Security Considerations
///
/// - This always derives account 0 - the most commonly used account
/// - Multiple calls with the same mnemonic + passphrase produce the same keypair
/// - Store mnemonics securely offline (paper wallet, hardware wallet, secure vault)
#[cfg(all(feature = "mnemonic", feature = "hd-wallet"))]
pub fn keypair_from_mnemonic(mnemonic: &str, passphrase: Option<&str>) -> Result<Keypair> {
    derive_keypair(mnemonic, passphrase, 0)
}

/// Convenience function to derive child keypairs without a passphrase.
///
/// This is a convenience wrapper around [`derive_keypair`] that omits the passphrase
/// parameter, making it simpler to derive multiple accounts from the same mnemonic.
/// Use this when you need multiple accounts but don't require passphrase protection.
///
/// # Arguments
///
/// * `mnemonic` - A valid BIP39 mnemonic phrase (12, 15, 18, 21, or 24 words)
/// * `account_index` - The account index in the derivation path (hardened)
///
/// # Derivation Path
///
/// Derives at: `m/44'/756'/{account_index}'/0'`
/// - `44'` - BIP44 purpose (hardened)
/// - `756'` - Rialo coin type (hardened)
/// - `{account_index}'` - Your specified account number (hardened)
/// - `0'` - Change address (hardened)
///
/// # Returns
///
/// Returns a `Result` containing the derived Ed25519 `Keypair` for the specified account.
///
/// # Errors
///
/// Returns `RialoError::Keyring` if:
/// - The mnemonic phrase is invalid or malformed
/// - The derivation path cannot be constructed
/// - Key derivation fails
///
/// # Examples
///
/// ```
/// use rialo_cdk::wallet::mnemonic;
///
/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
///
/// // Derive multiple accounts from the same mnemonic
/// let account_0 = mnemonic::derive_child_keypair(mnemonic, 0).unwrap();
/// let account_1 = mnemonic::derive_child_keypair(mnemonic, 1).unwrap();
/// let account_2 = mnemonic::derive_child_keypair(mnemonic, 2).unwrap();
///
/// // Each account has a different keypair
/// assert_ne!(
///     account_0.verifying_key().as_bytes(),
///     account_1.verifying_key().as_bytes()
/// );
/// ```
///
/// # When to Use
///
/// - **Use this** when deriving multiple accounts without passphrase protection
/// - **Use this** for simpler API when you don't need passphrases
/// - **Use [`derive_keypair`]** when you need passphrase protection
/// - **Use [`keypair_from_mnemonic`]** when you only need the default account 0
///
/// # Security Considerations
///
/// - This function does not use a passphrase - consider [`derive_keypair`] for additional security
/// - Each account index produces a cryptographically independent keypair
/// - The same mnemonic + account_index always produces the same keypair
/// - All accounts share the same mnemonic - if the mnemonic is compromised, all accounts are compromised
///
/// # Compatibility
///
/// This implementation is compatible with:
/// - Any wallet following BIP44 with Rialo coin type 756
/// - Other Rialo wallet implementations using the standard derivation path
#[cfg(all(feature = "mnemonic", feature = "hd-wallet"))]
pub fn derive_child_keypair(mnemonic: &str, account_index: u32) -> Result<Keypair> {
    derive_keypair(mnemonic, None, account_index)
}