Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding import from seed for wallet recovery web client #710

Open
wants to merge 7 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.8.0 (TBD)

### Features

* Added wallet generation from seed & import from seed on web sdk (#710)

## 0.7.0 (2025-01-28)

### Features
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions crates/rust-client/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use miden_objects::{account::AuthSecretKey, crypto::rand::FeltRng, Word};

use super::Client;
use crate::{
rpc::domain::account::AccountDetails,
store::{AccountRecord, AccountStatus},
ClientError,
};
Expand Down Expand Up @@ -251,6 +252,16 @@ impl<R: FeltRng> Client<R> {
.await?
.ok_or(ClientError::AccountDataNotFound(account_id))
}

pub async fn get_account_details(
&mut self,
account_id: AccountId,
) -> Result<AccountDetails, ClientError> {
match self.rpc_api.get_account_update(account_id).await {
Ok(details) => Ok(details),
Err(e) => Err(ClientError::RpcError(e)),
}
}
}

// TESTS
Expand Down
7 changes: 7 additions & 0 deletions crates/rust-client/src/rpc/domain/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ impl AccountDetails {
Self::Private(_, summary) | Self::Public(_, summary) => summary.hash,
}
}

pub fn account(&self) -> Option<&Account> {
match self {
Self::Private(..) => None,
Self::Public(account, _) => Some(account),
}
}
}

// ACCOUNT UPDATE SUMMARY
Expand Down
62 changes: 62 additions & 0 deletions crates/web-client/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use miden_client::{
account::{Account, AccountBuilder, AccountType},
crypto::{RpoRandomCoin, SecretKey},
Client,
};
use miden_lib::account::{auth::RpoFalcon512, wallets::BasicWallet};
use miden_objects::Felt;
use rand::{rngs::StdRng, Rng, SeedableRng};
use wasm_bindgen::JsValue;

use crate::models::account_storage_mode::AccountStorageMode;

pub async fn generate_account(
client: &mut Client<RpoRandomCoin>,
storage_mode: &AccountStorageMode,
mutable: bool,
seed: Option<Vec<u8>>,
) -> Result<(Account, [Felt; 4], SecretKey), JsValue> {
let mut rng = match seed {
Some(seed_bytes) => {
if seed_bytes.len() == 32 {
let mut seed_array = [0u8; 32];
seed_array.copy_from_slice(&seed_bytes);
let mut std_rng = StdRng::from_seed(seed_array);
let coin_seed: [u64; 4] = std_rng.gen();
&mut RpoRandomCoin::new(coin_seed.map(Felt::new))
} else {
Err(JsValue::from_str("Seed must be exactly 32 bytes"))?
}
},
None => client.rng(),
};
let key_pair = SecretKey::with_rng(&mut rng);

let mut init_seed = [0u8; 32];
rng.fill_bytes(&mut init_seed);

let account_type = if mutable {
AccountType::RegularAccountUpdatableCode
} else {
AccountType::RegularAccountImmutableCode
};

let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, account_seed) = match AccountBuilder::new(init_seed)
.anchor((&anchor_block).try_into().unwrap())
.account_type(account_type)
.storage_mode(storage_mode.into())
.with_component(RpoFalcon512::new(key_pair.public_key()))
.with_component(BasicWallet)
.build()
{
Ok(result) => result,
Err(err) => {
let error_message = format!("Failed to create new wallet: {:?}", err);
return Err(JsValue::from_str(&error_message));
},
};

Ok((new_account, account_seed, key_pair))
}
72 changes: 71 additions & 1 deletion crates/web-client/src/import.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use miden_client::auth::AuthSecretKey;
use miden_objects::{account::AccountData, note::NoteFile, utils::Deserializable};
use serde_wasm_bindgen::from_value;
use wasm_bindgen::prelude::*;

use crate::WebClient;
use crate::{
helpers::generate_account, models::account_storage_mode::AccountStorageMode, WebClient,
};

#[wasm_bindgen]
impl WebClient {
Expand Down Expand Up @@ -36,6 +39,73 @@ impl WebClient {
}
}

pub async fn import_account_from_seed(
&mut self,
init_seed: Vec<u8>,
storage_mode: &AccountStorageMode,
mutable: bool,
) -> Result<JsValue, JsValue> {
if let Some(client) = self.get_mut_inner() {
let (generated_acct, account_seed, key_pair) =
generate_account(client, storage_mode, mutable, Some(init_seed)).await?;

if storage_mode.is_public() {
// If public, fetch the data from chain
let account_details =
client.get_account_details(generated_acct.id()).await.map_err(|err| {
JsValue::from_str(&format!("Failed to get account details: {}", err))
})?;

let on_chain_account = account_details.account();

match on_chain_account {
Some(account) => {
match client
.add_account(
account,
Some(account_seed),
&AuthSecretKey::RpoFalcon512(key_pair),
false,
)
.await
{
Ok(_) => {
let message = format!("Imported account with ID: {}", account.id());
Ok(JsValue::from_str(&message))
},
Err(err) => {
let error_message = format!("Failed to import account: {:?}", err);
Err(JsValue::from_str(&error_message))
},
}
},
None => Err(JsValue::from_str("Account not found on chain")),
}
} else {
// Simply re-generate the account and insert it, without fetching any data
match client
.add_account(
&generated_acct,
Some(account_seed),
&AuthSecretKey::RpoFalcon512(key_pair),
false,
)
.await
{
Ok(_) => {
let message = format!("Imported account with ID: {}", generated_acct.id());
Ok(JsValue::from_str(&message))
},
Err(err) => {
let error_message = format!("Failed to import account: {:?}", err);
Err(JsValue::from_str(&error_message))
},
}
}
} else {
Err(JsValue::from_str("Client not initialized"))
}
}
pub async fn import_note(&mut self, note_bytes: JsValue) -> Result<JsValue, JsValue> {
if let Some(client) = self.get_mut_inner() {
let note_bytes_result: Vec<u8> = from_value(note_bytes).unwrap();
Expand Down
1 change: 1 addition & 0 deletions crates/web-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use wasm_bindgen::prelude::*;

pub mod account;
pub mod export;
pub mod helpers;
pub mod import;
pub mod models;
pub mod new_account;
Expand Down
6 changes: 6 additions & 0 deletions crates/web-client/src/models/account_storage_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ impl From<&AccountStorageMode> for NativeAccountStorageMode {
storage_mode.0
}
}

impl AccountStorageMode {
pub fn is_public(&self) -> bool {
self.0 == NativeAccountStorageMode::Public
}
}
41 changes: 7 additions & 34 deletions crates/web-client/src/new_account.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,31 @@
use miden_client::{
account::{
component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
AccountBuilder, AccountType,
},
account::{AccountBuilder, AccountType},
auth::AuthSecretKey,
crypto::SecretKey,
Felt,
};
use miden_lib::account::{auth::RpoFalcon512, faucets::BasicFungibleFaucet};
use miden_objects::asset::TokenSymbol;
use wasm_bindgen::prelude::*;

use super::models::{account::Account, account_storage_mode::AccountStorageMode};
use crate::WebClient;
use crate::{helpers::generate_account, WebClient};

#[wasm_bindgen]
impl WebClient {
pub async fn new_wallet(
&mut self,
storage_mode: &AccountStorageMode,
mutable: bool,
init_seed: Option<Vec<u8>>,
) -> Result<Account, JsValue> {
if let Some(client) = self.get_mut_inner() {
let key_pair = SecretKey::with_rng(client.rng());

let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);

let account_type = if mutable {
AccountType::RegularAccountUpdatableCode
} else {
AccountType::RegularAccountImmutableCode
};

let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, seed) = match AccountBuilder::new(init_seed)
.anchor((&anchor_block).try_into().unwrap())
.account_type(account_type)
.storage_mode(storage_mode.into())
.with_component(RpoFalcon512::new(key_pair.public_key()))
.with_component(BasicWallet)
.build()
{
Ok(result) => result,
Err(err) => {
let error_message = format!("Failed to create new wallet: {:?}", err);
return Err(JsValue::from_str(&error_message));
},
};

let (new_account, account_seed, key_pair) =
generate_account(client, storage_mode, mutable, init_seed).await?;
match client
.add_account(
&new_account,
Some(seed),
Some(account_seed),
&AuthSecretKey::RpoFalcon512(key_pair),
false,
)
Expand Down
70 changes: 70 additions & 0 deletions crates/web-client/test/import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect } from "chai";
import { testingPage } from "./mocha.global.setup.mjs";
import {
clearStore,
createNewFaucet,
createNewWallet,
fundAccountFromFaucet,
getAccountBalance,
StorageMode,
} from "./webClientTestUtils";

const importWalletFromSeed = async (
walletSeed: Uint8Array,
storageMode: StorageMode,
mutable: boolean
) => {
const serializedWalletSeed = Array.from(walletSeed);
return await testingPage.evaluate(
async (_serializedWalletSeed, _storageMode, _mutable) => {
const client = window.client;
const _walletSeed = new Uint8Array(_serializedWalletSeed);

const accountStorageMode =
_storageMode === "private"
? window.AccountStorageMode.private()
: window.AccountStorageMode.public();

await client.import_account_from_seed(
_walletSeed,
accountStorageMode,
_mutable
);
},
serializedWalletSeed,
storageMode,
mutable
);
};

describe("import from seed", () => {
it("should import same public account from seed", async () => {
const walletSeed = new Uint8Array(32);
crypto.getRandomValues(walletSeed);

const mutable = false;
const storageMode = StorageMode.PUBLIC;

const initialWallet = await createNewWallet({
storageMode,
mutable,
walletSeed,
});
const faucet = await createNewFaucet();

const result = await fundAccountFromFaucet(initialWallet.id, faucet.id);
const initialBalance = result.targetAccountBalanace;

// Deleting the account
await clearStore();

await importWalletFromSeed(walletSeed, storageMode, mutable);

const restoredBalance = await getAccountBalance(
initialWallet.id,
faucet.id
);

expect(restoredBalance.toString()).to.equal(initialBalance);
});
});
Loading