mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-02-18 20:33:13 +00:00
fix(wallet): use cryptographically secure entropy for mnemonic generation
The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
This commit is contained in:
parent
8cf73c00a9
commit
a3983f5a89
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -9026,6 +9026,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"base64 0.22.1",
|
||||
"bip39",
|
||||
"borsh",
|
||||
"bytemuck",
|
||||
"clap",
|
||||
|
||||
545
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
545
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
@ -0,0 +1,545 @@
|
||||
{
|
||||
"sequencer_addr": "http://127.0.0.1:3040",
|
||||
"seq_poll_timeout_millis": 12000,
|
||||
"seq_tx_poll_max_blocks": 5,
|
||||
"seq_poll_max_retries": 5,
|
||||
"seq_block_poll_max_amount": 100,
|
||||
"initial_accounts": [
|
||||
{
|
||||
"Public": {
|
||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
||||
"pub_sign_key": [
|
||||
16,
|
||||
162,
|
||||
106,
|
||||
154,
|
||||
236,
|
||||
125,
|
||||
52,
|
||||
184,
|
||||
35,
|
||||
100,
|
||||
238,
|
||||
174,
|
||||
69,
|
||||
197,
|
||||
41,
|
||||
77,
|
||||
187,
|
||||
10,
|
||||
118,
|
||||
75,
|
||||
0,
|
||||
11,
|
||||
148,
|
||||
238,
|
||||
185,
|
||||
181,
|
||||
133,
|
||||
17,
|
||||
220,
|
||||
72,
|
||||
124,
|
||||
77
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Public": {
|
||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
||||
"pub_sign_key": [
|
||||
113,
|
||||
121,
|
||||
64,
|
||||
177,
|
||||
204,
|
||||
85,
|
||||
229,
|
||||
214,
|
||||
178,
|
||||
6,
|
||||
109,
|
||||
191,
|
||||
29,
|
||||
154,
|
||||
63,
|
||||
38,
|
||||
242,
|
||||
18,
|
||||
244,
|
||||
219,
|
||||
8,
|
||||
208,
|
||||
35,
|
||||
136,
|
||||
23,
|
||||
127,
|
||||
207,
|
||||
237,
|
||||
216,
|
||||
169,
|
||||
190,
|
||||
27
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Private": {
|
||||
"account_id": "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw",
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
},
|
||||
"key_chain": {
|
||||
"secret_spending_key": [
|
||||
251,
|
||||
82,
|
||||
235,
|
||||
1,
|
||||
146,
|
||||
96,
|
||||
30,
|
||||
81,
|
||||
162,
|
||||
234,
|
||||
33,
|
||||
15,
|
||||
123,
|
||||
129,
|
||||
116,
|
||||
0,
|
||||
84,
|
||||
136,
|
||||
176,
|
||||
70,
|
||||
190,
|
||||
224,
|
||||
161,
|
||||
54,
|
||||
134,
|
||||
142,
|
||||
154,
|
||||
1,
|
||||
18,
|
||||
251,
|
||||
242,
|
||||
189
|
||||
],
|
||||
"private_key_holder": {
|
||||
"nullifier_secret_key": [
|
||||
29,
|
||||
250,
|
||||
10,
|
||||
187,
|
||||
35,
|
||||
123,
|
||||
180,
|
||||
250,
|
||||
246,
|
||||
97,
|
||||
216,
|
||||
153,
|
||||
44,
|
||||
156,
|
||||
16,
|
||||
93,
|
||||
241,
|
||||
26,
|
||||
174,
|
||||
219,
|
||||
72,
|
||||
84,
|
||||
34,
|
||||
247,
|
||||
112,
|
||||
101,
|
||||
217,
|
||||
243,
|
||||
189,
|
||||
173,
|
||||
75,
|
||||
20
|
||||
],
|
||||
"incoming_viewing_secret_key": [
|
||||
251,
|
||||
201,
|
||||
22,
|
||||
154,
|
||||
100,
|
||||
165,
|
||||
218,
|
||||
108,
|
||||
163,
|
||||
190,
|
||||
135,
|
||||
91,
|
||||
145,
|
||||
84,
|
||||
69,
|
||||
241,
|
||||
46,
|
||||
117,
|
||||
217,
|
||||
110,
|
||||
197,
|
||||
248,
|
||||
91,
|
||||
193,
|
||||
14,
|
||||
104,
|
||||
88,
|
||||
103,
|
||||
67,
|
||||
153,
|
||||
182,
|
||||
158
|
||||
],
|
||||
"outgoing_viewing_secret_key": [
|
||||
25,
|
||||
67,
|
||||
121,
|
||||
76,
|
||||
175,
|
||||
100,
|
||||
30,
|
||||
198,
|
||||
105,
|
||||
123,
|
||||
49,
|
||||
169,
|
||||
75,
|
||||
178,
|
||||
75,
|
||||
210,
|
||||
100,
|
||||
143,
|
||||
210,
|
||||
243,
|
||||
228,
|
||||
243,
|
||||
21,
|
||||
18,
|
||||
36,
|
||||
84,
|
||||
164,
|
||||
186,
|
||||
139,
|
||||
113,
|
||||
214,
|
||||
12
|
||||
]
|
||||
},
|
||||
"nullifer_public_key": [
|
||||
63,
|
||||
202,
|
||||
178,
|
||||
231,
|
||||
183,
|
||||
82,
|
||||
237,
|
||||
212,
|
||||
216,
|
||||
221,
|
||||
215,
|
||||
255,
|
||||
153,
|
||||
101,
|
||||
177,
|
||||
161,
|
||||
254,
|
||||
210,
|
||||
128,
|
||||
122,
|
||||
54,
|
||||
190,
|
||||
230,
|
||||
151,
|
||||
183,
|
||||
64,
|
||||
225,
|
||||
229,
|
||||
113,
|
||||
1,
|
||||
228,
|
||||
97
|
||||
],
|
||||
"incoming_viewing_public_key": [
|
||||
3,
|
||||
235,
|
||||
139,
|
||||
131,
|
||||
237,
|
||||
177,
|
||||
122,
|
||||
189,
|
||||
6,
|
||||
177,
|
||||
167,
|
||||
178,
|
||||
202,
|
||||
117,
|
||||
246,
|
||||
58,
|
||||
28,
|
||||
65,
|
||||
132,
|
||||
79,
|
||||
220,
|
||||
139,
|
||||
119,
|
||||
243,
|
||||
187,
|
||||
160,
|
||||
212,
|
||||
121,
|
||||
61,
|
||||
247,
|
||||
116,
|
||||
72,
|
||||
205
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Private": {
|
||||
"account_id": "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX",
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
},
|
||||
"key_chain": {
|
||||
"secret_spending_key": [
|
||||
238,
|
||||
171,
|
||||
241,
|
||||
69,
|
||||
111,
|
||||
217,
|
||||
85,
|
||||
64,
|
||||
19,
|
||||
82,
|
||||
18,
|
||||
189,
|
||||
32,
|
||||
91,
|
||||
78,
|
||||
175,
|
||||
107,
|
||||
7,
|
||||
109,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
243,
|
||||
230,
|
||||
72,
|
||||
244,
|
||||
192,
|
||||
92,
|
||||
137,
|
||||
33,
|
||||
118,
|
||||
254
|
||||
],
|
||||
"private_key_holder": {
|
||||
"nullifier_secret_key": [
|
||||
25,
|
||||
211,
|
||||
215,
|
||||
119,
|
||||
57,
|
||||
223,
|
||||
247,
|
||||
37,
|
||||
245,
|
||||
144,
|
||||
122,
|
||||
29,
|
||||
118,
|
||||
245,
|
||||
83,
|
||||
228,
|
||||
23,
|
||||
9,
|
||||
101,
|
||||
120,
|
||||
88,
|
||||
33,
|
||||
238,
|
||||
207,
|
||||
128,
|
||||
61,
|
||||
110,
|
||||
2,
|
||||
89,
|
||||
62,
|
||||
164,
|
||||
13
|
||||
],
|
||||
"incoming_viewing_secret_key": [
|
||||
193,
|
||||
181,
|
||||
14,
|
||||
196,
|
||||
142,
|
||||
84,
|
||||
15,
|
||||
65,
|
||||
128,
|
||||
101,
|
||||
70,
|
||||
196,
|
||||
241,
|
||||
47,
|
||||
130,
|
||||
221,
|
||||
23,
|
||||
146,
|
||||
161,
|
||||
237,
|
||||
221,
|
||||
40,
|
||||
19,
|
||||
126,
|
||||
59,
|
||||
15,
|
||||
169,
|
||||
236,
|
||||
25,
|
||||
105,
|
||||
104,
|
||||
231
|
||||
],
|
||||
"outgoing_viewing_secret_key": [
|
||||
20,
|
||||
170,
|
||||
220,
|
||||
108,
|
||||
41,
|
||||
23,
|
||||
155,
|
||||
217,
|
||||
247,
|
||||
190,
|
||||
175,
|
||||
168,
|
||||
247,
|
||||
34,
|
||||
105,
|
||||
134,
|
||||
114,
|
||||
74,
|
||||
104,
|
||||
91,
|
||||
211,
|
||||
62,
|
||||
126,
|
||||
13,
|
||||
130,
|
||||
100,
|
||||
241,
|
||||
214,
|
||||
250,
|
||||
236,
|
||||
38,
|
||||
150
|
||||
]
|
||||
},
|
||||
"nullifer_public_key": [
|
||||
192,
|
||||
251,
|
||||
166,
|
||||
243,
|
||||
167,
|
||||
236,
|
||||
84,
|
||||
249,
|
||||
35,
|
||||
136,
|
||||
130,
|
||||
172,
|
||||
219,
|
||||
225,
|
||||
161,
|
||||
139,
|
||||
229,
|
||||
89,
|
||||
243,
|
||||
125,
|
||||
194,
|
||||
213,
|
||||
209,
|
||||
30,
|
||||
23,
|
||||
174,
|
||||
100,
|
||||
244,
|
||||
124,
|
||||
74,
|
||||
140,
|
||||
47
|
||||
],
|
||||
"incoming_viewing_public_key": [
|
||||
2,
|
||||
181,
|
||||
98,
|
||||
93,
|
||||
216,
|
||||
241,
|
||||
241,
|
||||
110,
|
||||
58,
|
||||
198,
|
||||
119,
|
||||
174,
|
||||
250,
|
||||
184,
|
||||
1,
|
||||
204,
|
||||
200,
|
||||
173,
|
||||
44,
|
||||
238,
|
||||
37,
|
||||
247,
|
||||
170,
|
||||
156,
|
||||
100,
|
||||
254,
|
||||
116,
|
||||
242,
|
||||
28,
|
||||
183,
|
||||
187,
|
||||
77,
|
||||
255
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -218,7 +218,7 @@ impl TestContext {
|
||||
let config_overrides = WalletConfigOverrides::default();
|
||||
|
||||
let wallet_password = "test_pass".to_owned();
|
||||
let wallet = WalletCore::new_init_storage(
|
||||
let (wallet, _mnemonic) = WalletCore::new_init_storage(
|
||||
config_path,
|
||||
storage_path,
|
||||
Some(config_overrides),
|
||||
|
||||
@ -205,13 +205,14 @@ fn new_wallet_rust_with_default_config(password: &str) -> WalletCore {
|
||||
let config_path = tempdir.path().join("wallet_config.json");
|
||||
let storage_path = tempdir.path().join("storage.json");
|
||||
|
||||
WalletCore::new_init_storage(
|
||||
let (core, _mnemonic) = WalletCore::new_init_storage(
|
||||
config_path.to_path_buf(),
|
||||
storage_path.to_path_buf(),
|
||||
None,
|
||||
password.to_string(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
core
|
||||
}
|
||||
|
||||
fn load_existing_ffi_wallet(home: &Path) -> *mut WalletHandle {
|
||||
|
||||
@ -40,10 +40,10 @@ impl KeyChain {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_mnemonic(passphrase);
|
||||
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||
@ -51,12 +51,13 @@ impl KeyChain {
|
||||
let nullifer_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
||||
|
||||
Self {
|
||||
(Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifer_public_key,
|
||||
viewing_public_key,
|
||||
}
|
||||
},
|
||||
mnemonic)
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
|
||||
@ -8,8 +8,6 @@ use rand::{RngCore, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||
/// Produces `TopSecretKeyHolder` objects.
|
||||
@ -46,9 +44,23 @@ impl SeedHolder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) {
|
||||
let mut entropy_bytes: [u8; 32] = [0; 32];
|
||||
OsRng.fill_bytes(&mut entropy_bytes);
|
||||
|
||||
let mnemonic =
|
||||
Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
(
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
},
|
||||
mnemonic,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self {
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
Self {
|
||||
@ -163,12 +175,63 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_seeds_generated_same_from_same_mnemonic() {
|
||||
let mnemonic = "test_pass";
|
||||
fn two_seeds_recovered_same_from_same_mnemonic() {
|
||||
let passphrase = "test_pass";
|
||||
|
||||
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
// Generate a mnemonic with random entropy
|
||||
let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
|
||||
assert_eq!(seed_holder1.seed, seed_holder2.seed);
|
||||
// Recover from the same mnemonic
|
||||
let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase);
|
||||
|
||||
assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_generates_different_seeds_each_time() {
|
||||
let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic("");
|
||||
let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic("");
|
||||
|
||||
// Different entropy should produce different mnemonics and seeds
|
||||
assert_ne!(mnemonic1.to_string(), mnemonic2.to_string());
|
||||
assert_ne!(seed_holder1.seed, seed_holder2.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_generates_24_word_phrase() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
// 256 bits of entropy produces a 24-word mnemonic
|
||||
let word_count = mnemonic.to_string().split_whitespace().count();
|
||||
assert_eq!(word_count, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_produces_valid_seed_length() {
|
||||
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_passphrases_produce_different_seeds() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a");
|
||||
let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b");
|
||||
|
||||
// Same mnemonic but different passphrases should produce different seeds
|
||||
assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_passphrase_is_deterministic() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
let seed1 = SeedHolder::from_mnemonic(&mnemonic, "");
|
||||
let seed2 = SeedHolder::from_mnemonic(&mnemonic, "");
|
||||
|
||||
// Same mnemonic and passphrase should always produce the same seed
|
||||
assert_eq!(seed1.seed, seed2.seed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,11 +187,12 @@ impl NSSAUserData {
|
||||
|
||||
impl Default for NSSAUserData {
|
||||
fn default() -> Self {
|
||||
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
||||
Self::new_with_accounts(
|
||||
BTreeMap::new(),
|
||||
BTreeMap::new(),
|
||||
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
KeyTreePublic::new(&seed_holder),
|
||||
KeyTreePrivate::new(&seed_holder),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ pub unsafe extern "C" fn wallet_ffi_create_new(
|
||||
};
|
||||
|
||||
match WalletCore::new_init_storage(config_path, storage_path, None, password) {
|
||||
Ok(core) => {
|
||||
Ok((core, _mnemonic)) => {
|
||||
let wrapper = Box::new(WalletWrapper {
|
||||
core: Mutex::new(core),
|
||||
});
|
||||
|
||||
@ -344,6 +344,30 @@ enum WalletFfiError wallet_ffi_get_account_public(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *account_id,
|
||||
struct FfiAccount *out_account);
|
||||
|
||||
/**
|
||||
* Get full private account data from the local storage.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `account_id`: The account ID (32 bytes)
|
||||
* - `out_account`: Output pointer for account data
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` on successful query
|
||||
* - Error code on failure
|
||||
*
|
||||
* # Memory
|
||||
* The account data must be freed with `wallet_ffi_free_account_data()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `account_id` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `out_account` must be a valid pointer to a `FfiAccount` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_get_account_private(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *account_id,
|
||||
struct FfiAccount *out_account);
|
||||
|
||||
/**
|
||||
* Free account data returned by `wallet_ffi_get_account_public`.
|
||||
*
|
||||
@ -546,6 +570,108 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Send a shielded token transfer.
|
||||
*
|
||||
* Transfers tokens from a public account to a private account.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `from`: Source account ID (must be owned by this wallet)
|
||||
* - `to_keys`: Destination account keys
|
||||
* - `amount`: Amount to transfer as little-endian [u8; 16]
|
||||
* - `out_result`: Output pointer for transfer result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the transfer was submitted successfully
|
||||
* - `InsufficientFunds` if the source account doesn't have enough balance
|
||||
* - `KeyNotFound` if the source account's signing key is not in this wallet
|
||||
* - Error code on other failures
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `from` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `to_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
|
||||
* - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *from,
|
||||
const struct FfiPrivateAccountKeys *to_keys,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Send a deshielded token transfer.
|
||||
*
|
||||
* Transfers tokens from a private account to a public account.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `from`: Source account ID (must be owned by this wallet)
|
||||
* - `to`: Destination account ID
|
||||
* - `amount`: Amount to transfer as little-endian [u8; 16]
|
||||
* - `out_result`: Output pointer for transfer result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the transfer was submitted successfully
|
||||
* - `InsufficientFunds` if the source account doesn't have enough balance
|
||||
* - `KeyNotFound` if the source account's signing key is not in this wallet
|
||||
* - Error code on other failures
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `from` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `to` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *from,
|
||||
const struct FfiBytes32 *to,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Send a private token transfer.
|
||||
*
|
||||
* Transfers tokens from a private account to another private account.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `from`: Source account ID (must be owned by this wallet)
|
||||
* - `to_keys`: Destination account keys
|
||||
* - `amount`: Amount to transfer as little-endian [u8; 16]
|
||||
* - `out_result`: Output pointer for transfer result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the transfer was submitted successfully
|
||||
* - `InsufficientFunds` if the source account doesn't have enough balance
|
||||
* - `KeyNotFound` if the source account's signing key is not in this wallet
|
||||
* - Error code on other failures
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `from` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `to_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
|
||||
* - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_transfer_private(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *from,
|
||||
const struct FfiPrivateAccountKeys *to_keys,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Register a public account on the network.
|
||||
*
|
||||
@ -573,6 +699,33 @@ enum WalletFfiError wallet_ffi_register_public_account(struct WalletHandle *hand
|
||||
const struct FfiBytes32 *account_id,
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Register a private account on the network.
|
||||
*
|
||||
* This initializes a private account. The account must be
|
||||
* owned by this wallet.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `account_id`: Account ID to register
|
||||
* - `out_result`: Output pointer for registration result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the registration was submitted successfully
|
||||
* - Error code on failure
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `account_id` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_register_private_account(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *account_id,
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Free a transfer result returned by `wallet_ffi_transfer_public` or
|
||||
* `wallet_ffi_register_public_account`.
|
||||
|
||||
@ -11,6 +11,7 @@ common.workspace = true
|
||||
key_protocol.workspace = true
|
||||
token_core.workspace = true
|
||||
amm_core.workspace = true
|
||||
bip39.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::collections::{BTreeMap, HashMap, btree_map::Entry};
|
||||
|
||||
use anyhow::Result;
|
||||
use bip39::Mnemonic;
|
||||
use key_protocol::{
|
||||
key_management::{
|
||||
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
||||
@ -91,7 +92,7 @@ impl WalletChainStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_storage(config: WalletConfig, password: String) -> Result<Self> {
|
||||
pub fn new_storage(config: WalletConfig, password: String) -> Result<(Self, Mnemonic)> {
|
||||
let mut public_init_acc_map = BTreeMap::new();
|
||||
let mut private_init_acc_map = BTreeMap::new();
|
||||
|
||||
@ -111,13 +112,43 @@ impl WalletChainStore {
|
||||
}
|
||||
}
|
||||
|
||||
let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone()));
|
||||
let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password));
|
||||
// TODO: Use password for storage encryption
|
||||
let _ = password;
|
||||
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
let public_tree = KeyTreePublic::new(&seed_holder);
|
||||
let private_tree = KeyTreePrivate::new(&seed_holder);
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
user_data: NSSAUserData::new_with_accounts(
|
||||
public_init_acc_map,
|
||||
private_init_acc_map,
|
||||
public_tree,
|
||||
private_tree,
|
||||
)?,
|
||||
wallet_config: config,
|
||||
labels: HashMap::new(),
|
||||
},
|
||||
mnemonic,
|
||||
))
|
||||
}
|
||||
|
||||
/// Restore storage from an existing mnemonic phrase.
|
||||
pub fn restore_storage(
|
||||
config: WalletConfig,
|
||||
mnemonic: &Mnemonic,
|
||||
password: &str,
|
||||
) -> Result<Self> {
|
||||
// TODO: Use password for storage encryption
|
||||
let _ = password;
|
||||
let seed_holder = SeedHolder::from_mnemonic(mnemonic, "");
|
||||
let public_tree = KeyTreePublic::new(&seed_holder);
|
||||
let private_tree = KeyTreePrivate::new(&seed_holder);
|
||||
|
||||
Ok(Self {
|
||||
user_data: NSSAUserData::new_with_accounts(
|
||||
public_init_acc_map,
|
||||
private_init_acc_map,
|
||||
BTreeMap::new(),
|
||||
BTreeMap::new(),
|
||||
public_tree,
|
||||
private_tree,
|
||||
)?,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::{io::Write, path::PathBuf};
|
||||
use std::{io::Write, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bip39::Mnemonic;
|
||||
use clap::{Parser, Subcommand};
|
||||
use common::HashType;
|
||||
use nssa::{ProgramDeploymentTransaction, program::Program};
|
||||
@ -150,8 +151,9 @@ pub async fn execute_subcommand(
|
||||
config_subcommand.handle_subcommand(wallet_core).await?
|
||||
}
|
||||
Command::RestoreKeys { depth } => {
|
||||
let mnemonic = read_mnemonic_from_stdin()?;
|
||||
let password = read_password_from_stdin()?;
|
||||
wallet_core.reset_storage(password)?;
|
||||
wallet_core.restore_storage(&mnemonic, &password)?;
|
||||
execute_keys_restoration(wallet_core, depth).await?;
|
||||
|
||||
SubcommandReturnValue::Empty
|
||||
@ -202,6 +204,16 @@ pub fn read_password_from_stdin() -> Result<String> {
|
||||
Ok(password.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
|
||||
let mut phrase = String::new();
|
||||
|
||||
print!("Input recovery phrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
std::io::stdin().read_line(&mut phrase)?;
|
||||
|
||||
Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase")
|
||||
}
|
||||
|
||||
pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> {
|
||||
wallet_core
|
||||
.storage
|
||||
|
||||
@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use bip39::Mnemonic;
|
||||
use chain_storage::WalletChainStore;
|
||||
use common::{
|
||||
HashType, error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse,
|
||||
@ -87,14 +88,23 @@ impl WalletCore {
|
||||
storage_path: PathBuf,
|
||||
config_overrides: Option<WalletConfigOverrides>,
|
||||
password: String,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
) -> Result<(Self, Mnemonic)> {
|
||||
let mut mnemonic_out = None;
|
||||
let wallet = Self::new(
|
||||
config_path,
|
||||
storage_path,
|
||||
config_overrides,
|
||||
|config| WalletChainStore::new_storage(config, password),
|
||||
|config| {
|
||||
let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?;
|
||||
mnemonic_out = Some(mnemonic);
|
||||
Ok(storage)
|
||||
},
|
||||
0,
|
||||
)
|
||||
)?;
|
||||
Ok((
|
||||
wallet,
|
||||
mnemonic_out.expect("mnemonic should be set after new_storage"),
|
||||
))
|
||||
}
|
||||
|
||||
fn new(
|
||||
@ -139,9 +149,13 @@ impl WalletCore {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
/// Reset storage
|
||||
pub fn reset_storage(&mut self, password: String) -> Result<()> {
|
||||
self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?;
|
||||
/// Restore storage from an existing mnemonic phrase.
|
||||
pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> {
|
||||
self.storage = WalletChainStore::restore_storage(
|
||||
self.storage.wallet_config.clone(),
|
||||
mnemonic,
|
||||
password,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -39,13 +39,21 @@ async fn main() -> Result<()> {
|
||||
println!("Persistent storage not found, need to execute setup");
|
||||
|
||||
let password = read_password_from_stdin()?;
|
||||
let wallet = WalletCore::new_init_storage(
|
||||
let (wallet, mnemonic) = WalletCore::new_init_storage(
|
||||
config_path,
|
||||
storage_path,
|
||||
Some(config_overrides),
|
||||
password,
|
||||
)?;
|
||||
|
||||
println!();
|
||||
println!("IMPORTANT: Write down your recovery phrase and store it securely.");
|
||||
println!("This is the only way to recover your wallet if you lose access.");
|
||||
println!();
|
||||
println!("Recovery phrase:");
|
||||
println!(" {}", mnemonic);
|
||||
println!();
|
||||
|
||||
wallet.store_persistent_data().await?;
|
||||
wallet
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user