Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* Incremented the limits for various RPC calls to accommodate larger data sets ([#1621](https://github.com/0xMiden/miden-client/pull/1621)).
* [BREAKING] Introduced named storage slots, changed `FilesystemKeystore` to not be generic over RNG ([#1626](https://github.com/0xMiden/miden-client/pull/1626)).
* Added `submit_new_transaction_with_prover` to the Rust client and `submitNewTransactionWithProver` to the WebClient([#1622](https://github.com/0xMiden/miden-client/pull/1622)).
* [BREAKING] Added E2E encryption for private notes to the client ([#1636](https://github.com/0xMiden/miden-client/pull/1636)).

## 0.12.5 (2025-12-01)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

15 changes: 10 additions & 5 deletions bin/integration-tests/src/tests/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use miden_client::account::AccountStorageMode;
use miden_client::address::{Address, AddressInterface, RoutingParameters};
use miden_client::asset::FungibleAsset;
use miden_client::auth::{AuthSchemeId, RPO_FALCON_SCHEME_ID};
use miden_client::note::NoteType;
Expand Down Expand Up @@ -159,10 +158,16 @@ async fn run_flow(
.await
.context("failed to insert faucet in sender")?;

// Build recipient address
let recipient_address = Address::new(recipient_account.id())
.with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet))
.context("failed to build recipient address")?;
// Get a recipient's address
let recipient_addresses = recipient
.test_store()
.get_addresses_by_account_id(recipient_account.id())
.await
.context("failed to get recipient addresses")?;
let recipient_address = recipient_addresses
.first()
.context("recipient should have a default address (with encryption key)")?
.clone();

// Ensure recipient has no input notes
recipient.sync_state().await.context("recipient initial sync")?;
Expand Down
5 changes: 5 additions & 0 deletions bin/miden-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,15 @@ impl Cli {
let keystore = FilesystemKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;

// Create encryption keystore (shares directory with auth keystore)
let encryption_keystore = FilesystemKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;

let mut builder = ClientBuilder::new()
.sqlite_store(cli_config.store_filepath.clone())
.grpc_client(&cli_config.rpc.endpoint.clone().into(), Some(cli_config.rpc.timeout_ms))
.authenticator(Arc::new(keystore.clone()))
.encryption_keystore(Arc::new(encryption_keystore))
.in_debug_mode(in_debug_mode)
.tx_graceful_blocks(Some(TX_GRACEFUL_BLOCK_DELTA));

Expand Down
30 changes: 15 additions & 15 deletions bin/miden-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,8 +749,7 @@ async fn list_addresses_add() -> Result<()> {
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert!(formatted_output.contains("Unspecified"));
assert!(!formatted_output.contains("BasicWallet"));
assert!(formatted_output.contains("BasicWallet"));

// Add a basic wallet address to the account
let mut add_address_cmd = cargo_bin_cmd!("miden-client");
Expand All @@ -765,14 +764,13 @@ async fn list_addresses_add() -> Result<()> {
let output = add_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());

// List of addresses for created account should now contain a BasicWallet address
// List of addresses for created account should now contain two BasicWallet addresses
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 2);

// Add another basic wallet address to the account
let mut add_address_cmd = cargo_bin_cmd!("miden-client");
Expand All @@ -787,14 +785,13 @@ async fn list_addresses_add() -> Result<()> {
let output = add_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());

// List of addresses for created account should now contain two BasicWallet addresses
// List of addresses for created account should now contain three BasicWallet addresses
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 2);
assert_eq!(formatted_output.matches("BasicWallet").count(), 3);

Ok(())
}
Expand All @@ -808,33 +805,33 @@ async fn list_addresses_remove() -> Result<()> {

sync_cli(&temp_dir);

// List of addresses for created account should contain an Unspecified address
// List of addresses for created account should contain a BasicWallet address
let mut list_addresses_cmd = cargo_bin_cmd!("miden-client");
list_addresses_cmd.args(["address", "list", &basic_account_id]);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 1);
assert_eq!(formatted_output.matches("BasicWallet").count(), 1);

// Remove the Unspecified wallet from the account
// Remove the BasicWallet address from the account
let mut remove_address_cmd = cargo_bin_cmd!("miden-client");
let unspecified_wallet_address = regex::Regex::new(r"mlcl1[0-9a-z]+")
let wallet_address = regex::Regex::new(r"mlcl1[0-9a-z_]+")
.unwrap()
.find(&formatted_output)
.unwrap()
.as_str();
remove_address_cmd.args(["address", "remove", &basic_account_id, unspecified_wallet_address]);
remove_address_cmd.args(["address", "remove", &basic_account_id, wallet_address]);
let output = remove_address_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());

// List of addresses for created account should now contain one BasicWallet address
// List of addresses for created account should now be empty
sync_cli(&temp_dir);
let output = list_addresses_cmd.current_dir(temp_dir.clone()).output().unwrap();
assert!(output.status.success());
let formatted_output = String::from_utf8(output.stdout).unwrap();
assert!(formatted_output.contains(&basic_account_id));
assert_eq!(formatted_output.matches("Unspecified").count(), 0);
assert_eq!(formatted_output.matches("BasicWallet").count(), 0);

Ok(())
}
Expand Down Expand Up @@ -1079,12 +1076,15 @@ async fn create_rust_client_with_store_path(

let keystore = FilesystemKeyStore::new(temp_dir())?;

let encryption_keystore = Arc::new(keystore.clone());

Ok((
TestClient::new(
Arc::new(GrpcClient::new(&endpoint, 10_000)),
rng,
store,
Some(std::sync::Arc::new(keystore.clone())),
Some(encryption_keystore),
ExecutionOptions::new(
Some(MAX_TX_EXECUTION_CYCLES),
MIN_TX_EXECUTION_CYCLES,
Expand Down
38 changes: 38 additions & 0 deletions crates/idxdb-store/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,41 @@ pub async fn get_account_auth_by_pub_key(pub_key: String) -> Result<String, JsVa
None => Err(JsValue::from_str(&format!("Pub key {pub_key} not found in the store"))),
}
}

// ENCRYPTION KEY STORAGE
// ================================================================================================

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncryptionKeyIdxdbObject {
pub key: String,
}

#[wasm_bindgen(module = "/src/js/accounts.js")]
extern "C" {
#[wasm_bindgen(js_name = insertEncryptionKey)]
pub fn idxdb_insert_encryption_key(address_hash: String, key: String) -> js_sys::Promise;

#[wasm_bindgen(js_name = getEncryptionKeyByAddressHash)]
pub fn idxdb_get_encryption_key_by_address_hash(address_hash: String) -> js_sys::Promise;
}

pub async fn insert_encryption_key(address_hash: String, key: String) -> Result<(), JsValue> {
let promise = idxdb_insert_encryption_key(address_hash, key);
JsFuture::from(promise).await?;
Ok(())
}

pub async fn get_encryption_key_by_address_hash(
address_hash: String,
) -> Result<Option<String>, JsValue> {
let promise = idxdb_get_encryption_key_by_address_hash(address_hash.clone());
let js_key = JsFuture::from(promise).await?;

let encryption_key_idxdb: Option<EncryptionKeyIdxdbObject> =
from_value(js_key).map_err(|err| {
JsValue::from_str(&format!("Error: failed to deserialize encryption key: {err}"))
})?;

Ok(encryption_key_idxdb.map(|k| k.key))
}
42 changes: 42 additions & 0 deletions crates/idxdb-store/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::from_value;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::{JsFuture, js_sys};

// WEB ENCRYPTION KEYSTORE HELPER
// ================================================================================================

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncryptionKeyIdxdbObject {
pub key: String,
}

#[wasm_bindgen(module = "/src/js/accounts.js")]
extern "C" {
#[wasm_bindgen(js_name = insertEncryptionKey)]
pub fn idxdb_insert_encryption_key(address_hash: String, key_hex: String) -> js_sys::Promise;

#[wasm_bindgen(js_name = getEncryptionKeyByAddressHash)]
pub fn idxdb_get_encryption_key(address_hash: String) -> js_sys::Promise;
}

pub async fn insert_encryption_key(address_hash: String, key_hex: String) -> Result<(), JsValue> {
let promise = idxdb_insert_encryption_key(address_hash, key_hex);
JsFuture::from(promise).await?;

Ok(())
}

pub async fn get_encryption_key(address_hash: String) -> Result<Option<String>, JsValue> {
let promise = idxdb_get_encryption_key(address_hash);
let js_key = JsFuture::from(promise).await?;

let encryption_key_idxdb: Option<EncryptionKeyIdxdbObject> =
from_value(js_key).map_err(|err| {
JsValue::from_str(&format!("Error: failed to deserialize encryption key: {err}"))
})?;

Ok(encryption_key_idxdb.map(|k| k.key))
}
31 changes: 30 additions & 1 deletion crates/idxdb-store/src/js/accounts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { accountCodes, accountStorages, accountAssets, accountAuths, accounts, addresses, foreignAccountCode, storageMapEntries, trackedAccounts, } from "./schema.js";
import { accountCodes, accountStorages, accountAssets, accountAuths, accounts, addresses, foreignAccountCode, storageMapEntries, trackedAccounts, encryptionKeys, } from "./schema.js";
import { logWebStoreError, uint8ArrayToBase64 } from "./utils.js";
// GET FUNCTIONS
export async function getAccountIds() {
Expand Down Expand Up @@ -422,3 +422,32 @@ export async function undoAccountStates(accountCommitments) {
logWebStoreError(error, `Error undoing account states: ${accountCommitments.join(",")}`);
}
}
// ENCRYPTION KEY FUNCTIONS
export async function insertEncryptionKey(addressHash, key) {
try {
const data = {
addressHash: addressHash,
key: key,
};
await encryptionKeys.put(data);
}
catch (error) {
logWebStoreError(error, `Error inserting encryption key for address hash: ${addressHash}`);
}
}
export async function getEncryptionKeyByAddressHash(addressHash) {
try {
const encryptionKey = await encryptionKeys
.where("addressHash")
.equals(addressHash)
.first();
if (!encryptionKey) {
return null;
}
return { key: encryptionKey.key };
}
catch (error) {
logWebStoreError(error, `Error getting encryption key for address hash: ${addressHash}`);
return null;
}
}
5 changes: 4 additions & 1 deletion crates/idxdb-store/src/js/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var Table;
Table["ForeignAccountCode"] = "foreignAccountCode";
Table["Settings"] = "settings";
Table["TrackedAccounts"] = "trackedAccounts";
Table["EncryptionKeys"] = "encryptionKeys";
})(Table || (Table = {}));
const db = new Dexie(DATABASE_NAME);
db.version(1).stores({
Expand All @@ -61,6 +62,7 @@ db.version(1).stores({
[Table.ForeignAccountCode]: indexes("accountId"),
[Table.Settings]: indexes("key"),
[Table.TrackedAccounts]: indexes("&id"),
[Table.EncryptionKeys]: indexes("&addressHash"),
});
function indexes(...items) {
return items.join(",");
Expand Down Expand Up @@ -90,6 +92,7 @@ const tags = db.table(Table.Tags);
const foreignAccountCode = db.table(Table.ForeignAccountCode);
const settings = db.table(Table.Settings);
const trackedAccounts = db.table(Table.TrackedAccounts);
const encryptionKeys = db.table(Table.EncryptionKeys);
async function ensureClientVersion(clientVersion) {
if (!clientVersion) {
console.warn("openDatabase called without a client version; skipping version enforcement.");
Expand Down Expand Up @@ -137,4 +140,4 @@ async function persistClientVersion(clientVersion) {
value: textEncoder.encode(clientVersion),
});
}
export { db, accountCodes, accountStorages, storageMapEntries, accountAssets, accountAuths, accounts, addresses, transactions, transactionScripts, inputNotes, outputNotes, notesScripts, stateSync, blockHeaders, partialBlockchainNodes, tags, foreignAccountCode, settings, trackedAccounts, };
export { db, accountCodes, accountStorages, storageMapEntries, accountAssets, accountAuths, accounts, addresses, transactions, transactionScripts, inputNotes, outputNotes, notesScripts, stateSync, blockHeaders, partialBlockchainNodes, tags, foreignAccountCode, settings, trackedAccounts, encryptionKeys, };
1 change: 1 addition & 0 deletions crates/idxdb-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use wasm_bindgen_futures::{JsFuture, js_sys};
pub mod account;
pub mod auth;
pub mod chain_data;
pub mod encryption;
pub mod export;
pub mod import;
pub mod note;
Expand Down
38 changes: 38 additions & 0 deletions crates/idxdb-store/src/ts/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
storageMapEntries,
IStorageMapEntry,
trackedAccounts,
encryptionKeys,
} from "./schema.js";
import { JsStorageMapEntry, JsStorageSlot, JsVaultAsset } from "./sync.js";
import { logWebStoreError, uint8ArrayToBase64 } from "./utils.js";
Expand Down Expand Up @@ -538,3 +539,40 @@ export async function undoAccountStates(accountCommitments: string[]) {
);
}
}

// ENCRYPTION KEY FUNCTIONS
export async function insertEncryptionKey(addressHash: string, key: string) {
try {
const data = {
addressHash: addressHash,
key: key,
};
await encryptionKeys.put(data);
} catch (error) {
logWebStoreError(
error,
`Error inserting encryption key for address hash: ${addressHash}`
);
}
}

export async function getEncryptionKeyByAddressHash(addressHash: string) {
try {
const encryptionKey = await encryptionKeys
.where("addressHash")
.equals(addressHash)
.first();

if (!encryptionKey) {
return null;
}

return { key: encryptionKey.key };
} catch (error) {
logWebStoreError(
error,
`Error getting encryption key for address hash: ${addressHash}`
);
return null;
}
}
Loading