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
21 changes: 14 additions & 7 deletions bin/miden-cli/src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use miden_client::{Client, PrettyPrint, Word, ZERO};
use crate::commands::new_account::load_packages;
use crate::config::{CliConfig, RpcConfig};
use crate::errors::CliError;
use crate::utils::{parse_account_id, split_procedure_target};
use crate::utils::{CLI_SETTING_DOMAIN, parse_account_id, split_procedure_target};
use crate::{client_binary_name, create_dynamic_table};

pub const DEFAULT_ACCOUNT_ID_KEY: &str = "default_account_id";
Expand Down Expand Up @@ -116,7 +116,7 @@ impl AccountCmd {
match id {
None => {
let default_account: AccountId = client
.get_setting(DEFAULT_ACCOUNT_ID_KEY.to_string())
.get_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?
.ok_or(CliError::Config(
"Default account".to_string().into(),
Expand All @@ -125,8 +125,9 @@ impl AccountCmd {
println!("Current default account ID: {default_account}");
},
Some(id) if id == "none" => {
let removed =
client.remove_setting(DEFAULT_ACCOUNT_ID_KEY.to_string()).await?;
let removed = client
.remove_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?;

if removed {
println!("Default account removed");
Expand All @@ -141,7 +142,11 @@ impl AccountCmd {
let (account, _) = client.account_reader(account_id).header().await?;

client
.set_setting(DEFAULT_ACCOUNT_ID_KEY.to_string(), account.id())
.set_setting(
&CLI_SETTING_DOMAIN,
DEFAULT_ACCOUNT_ID_KEY.to_string(),
account.id(),
)
.await?;

println!("Default account set to {}", account.id());
Expand Down Expand Up @@ -633,14 +638,16 @@ pub(crate) async fn set_default_account_if_unset<AUTH>(
account_id: AccountId,
) -> Result<(), CliError> {
if client
.get_setting::<AccountId>(DEFAULT_ACCOUNT_ID_KEY.to_string())
.get_setting::<AccountId>(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?
.is_some()
{
return Ok(());
}

client.set_setting(DEFAULT_ACCOUNT_ID_KEY.to_string(), account_id).await?;
client
.set_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string(), account_id)
.await?;

println!("Setting account {account_id} as the default account ID.");
println!(
Expand Down
6 changes: 4 additions & 2 deletions bin/miden-cli/src/commands/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::commands::new_account::load_packages;
use crate::config::CliConfig;
use crate::errors::CliError;
use crate::utils::{
CLI_SETTING_DOMAIN,
parse_account_id,
print_executed_program_stack,
print_executed_transaction,
Expand Down Expand Up @@ -351,8 +352,9 @@ async fn resolve_call_target<AUTH: Keystore + Sync + 'static>(
async fn pick_local_executor<AUTH: Keystore + Sync + 'static>(
client: &Client<AUTH>,
) -> Result<AccountId, CliError> {
let default_id: Option<AccountId> =
client.get_setting(DEFAULT_ACCOUNT_ID_KEY.to_string()).await?;
let default_id: Option<AccountId> = client
.get_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?;
if let Some(default_id) = default_id
&& let Some((_, status)) = client.get_account_header(default_id).await?
&& !status.is_locked()
Expand Down
3 changes: 2 additions & 1 deletion bin/miden-cli/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use miden_client::store::NoteFilter;
use super::config::CliConfig;
use crate::commands::account::DEFAULT_ACCOUNT_ID_KEY;
use crate::errors::CliError;
use crate::utils::CLI_SETTING_DOMAIN;

pub async fn print_client_info<AUTH: Keystore + Sync + 'static>(
client: &Client<AUTH>,
Expand Down Expand Up @@ -51,7 +52,7 @@ async fn print_client_stats<AUTH: Keystore + Sync + 'static>(
println!(
"Default account: {}",
client
.get_setting(DEFAULT_ACCOUNT_ID_KEY.to_string())
.get_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?
.map_or("-".to_string(), AccountId::to_hex)
);
Expand Down
28 changes: 17 additions & 11 deletions bin/miden-cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::sync::LazyLock;

use miden_client::account::{AccountId, FaucetMetadata};
use miden_client::address::{Address, AddressId};
use miden_client::asset::{Asset, FungibleAsset};
use miden_client::store::SettingDomain;
use miden_client::transaction::{ExecutedTransaction, InputNote};
use miden_client::utils::{base_units_to_tokens, tokens_to_base_units};
use miden_client::vm::MIN_STACK_DEPTH;
Expand Down Expand Up @@ -33,7 +35,7 @@ pub(crate) async fn get_input_acc_id_by_prefix_or_default<AUTH>(
account_id_prefix
} else {
client
.get_setting(DEFAULT_ACCOUNT_ID_KEY.to_string())
.get_setting(&CLI_SETTING_DOMAIN, DEFAULT_ACCOUNT_ID_KEY.to_string())
.await?
.map(AccountId::to_hex)
.ok_or(CliError::Input("No input account ID nor default account defined".to_string()))?
Expand Down Expand Up @@ -352,8 +354,9 @@ impl FaucetMetadataResolver {
return Ok(Some(FaucetMetadata { symbol, decimals }));
}
// 2) settings store
let setting_key = faucet_metadata_setting_key(faucet_id);
Ok(client.get_setting::<FaucetMetadata>(setting_key).await?)
Ok(client
.get_setting::<FaucetMetadata>(&FAUCET_METADATA_SETTING_DOMAIN, faucet_id.to_hex())
.await?)
}

/// Looks up `(symbol, decimals)` for a faucet, walking TOML → settings store → RPC fetch.
Expand All @@ -368,10 +371,12 @@ impl FaucetMetadataResolver {
return Ok(Some(meta));
}
// 3) RPC fetch
let setting_key = faucet_metadata_setting_key(faucet_id);
match client.fetch_remote_token_metadata(faucet_id).await {
Ok(Some(meta)) => {
if let Err(err) = client.set_setting(setting_key, meta.clone()).await {
if let Err(err) = client
.set_setting(&FAUCET_METADATA_SETTING_DOMAIN, faucet_id.to_hex(), meta.clone())
.await
{
tracing::warn!(
"failed to persist faucet metadata for {}: {err}",
faucet_id.to_hex(),
Expand Down Expand Up @@ -458,13 +463,14 @@ impl FaucetMetadataResolver {
}
}

/// Settings key prefix under which faucet display metadata is persisted.
const FAUCET_METADATA_SETTING_PREFIX: &str = "faucet_metadata:";
/// Settings domain the CLI persists its own state in.
pub(crate) static CLI_SETTING_DOMAIN: LazyLock<SettingDomain> =
LazyLock::new(|| SettingDomain::new("cli").expect("the CLI settings domain is not empty"));

/// Returns the settings-store key under which the metadata for `faucet_id` is persisted.
fn faucet_metadata_setting_key(faucet_id: AccountId) -> String {
format!("{FAUCET_METADATA_SETTING_PREFIX}{}", faucet_id.to_hex())
}
/// Settings domain holding cached faucet display metadata, keyed by the faucet's hex ID.
pub(crate) static FAUCET_METADATA_SETTING_DOMAIN: LazyLock<SettingDomain> = LazyLock::new(|| {
SettingDomain::new("faucet_metadata").expect("the faucet metadata domain is not empty")
});

/// Parses a bech32 address from the token symbol map.
fn parse_address(address_str: &str) -> Result<AccountId, String> {
Expand Down
5 changes: 3 additions & 2 deletions bin/miden-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use miden_client::keystore::Keystore;
use miden_client::note::NoteId;
use miden_client::note_transport::NOTE_TRANSPORT_TESTNET_ENDPOINT;
use miden_client::rpc::Endpoint;
use miden_client::store::SettingDomain;
use miden_client::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
use miden_client::testing::common::{
ACCOUNT_ID_REGULAR,
Expand Down Expand Up @@ -496,8 +497,8 @@ async fn public_faucet_metadata_is_fetched_and_persisted() -> Result<()> {
// Assert the resolver wrote the metadata into the settings store.
let faucet_id = AccountId::from_hex(&fungible_faucet_account_id).unwrap();
let (client, _) = create_rust_client_with_store_path(&store_path, endpoint).await?;
let setting_key = format!("faucet_metadata:{}", faucet_id.to_hex());
let stored: Option<FaucetMetadata> = client.get_setting(setting_key).await?;
let domain = SettingDomain::new("faucet_metadata")?;
let stored: Option<FaucetMetadata> = client.get_setting(&domain, faucet_id.to_hex()).await?;
assert!(
stored.is_some(),
"expected settings store to contain metadata for {fungible_faucet_account_id} after notes -s",
Expand Down
54 changes: 32 additions & 22 deletions crates/rust-client/src/note_transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,30 @@ use miden_tx::utils::serde::{
};

pub use self::errors::NoteTransportError;
use crate::store::SettingDomain;
use crate::sync::NoteTagSource;
use crate::{Client, ClientError};

pub const NOTE_TRANSPORT_TESTNET_ENDPOINT: &str = "https://transport.miden.io";
pub const NOTE_TRANSPORT_DEVNET_ENDPOINT: &str = "https://transport.devnet.miden.io";
pub const NOTE_TRANSPORT_CURSOR_STORE_SETTING: &str = "note_transport_cursor";

/// Settings domain holding the note-transport layer's persisted state.
pub(crate) const NOTE_TRANSPORT_SETTING_DOMAIN: &str = "note_transport";

pub(crate) const NOTE_TRANSPORT_CURSOR_SETTING: &str = "cursor";

/// Settings key for the note-transport backfill bookkeeping: a serialized `Vec<NoteTag>` of the
/// `User`- and `Account`-source tags whose full history has already been fetched up to the global
/// cursor. [`Client::sync_note_transport`] diffs the currently tracked tags against this set to
/// find tags added after the cursor advanced, and backfills only those. Reusing the settings k/v
/// avoids a Store-trait schema change while surviving process restarts.
pub const NOTE_TRANSPORT_COVERED_TAGS_KEY: &str = "note_transport_covered_tags";
/// find tags added after the cursor advanced, and backfills only those.
const NOTE_TRANSPORT_COVERED_TAGS_SETTING: &str = "covered_tags";

/// Settings key for the durable relay outbox: a serialized `Vec<NoteInfo>` of
/// private notes whose transport delivery has not yet succeeded.
/// `send_private_note` appends (replacing any entry with the same note id)
/// before relaying; [`Client::flush_relay_outbox`] drains entries that re-send
/// successfully. Reusing the settings k/v avoids a Store-trait schema change
/// while surviving process restarts.
pub const NOTE_TRANSPORT_OUTBOX_KEY: &str = "note_transport_outbox";
/// successfully. It survives process restarts.
const NOTE_TRANSPORT_OUTBOX_SETTING: &str = "outbox";

/// Client note transport methods.
impl<AUTH> Client<AUTH> {
Expand Down Expand Up @@ -223,9 +226,10 @@ impl<AUTH> Client<AUTH> {
/// an empty `Vec` is returned — leaving unreadable bytes in place would
/// block every subsequent relay because each sync would re-read them.
async fn load_relay_outbox(&self) -> Result<Vec<NoteInfo>, ClientError> {
let domain = SettingDomain::client(NOTE_TRANSPORT_SETTING_DOMAIN);
let bytes = self
.store
.get_setting(String::from(NOTE_TRANSPORT_OUTBOX_KEY))
.get_setting(&domain, String::from(NOTE_TRANSPORT_OUTBOX_SETTING))
.await
.map_err(ClientError::StoreError)?;
let Some(bytes) = bytes else {
Expand All @@ -236,7 +240,7 @@ impl<AUTH> Client<AUTH> {
Err(err) => {
tracing::warn!(?err, "dropping unreadable relay outbox; resetting to empty");
self.store
.remove_setting(String::from(NOTE_TRANSPORT_OUTBOX_KEY))
.remove_setting(&domain, String::from(NOTE_TRANSPORT_OUTBOX_SETTING))
.await
.map_err(ClientError::StoreError)?;
Ok(Vec::new())
Expand All @@ -247,13 +251,17 @@ impl<AUTH> Client<AUTH> {
/// Persist the relay outbox, removing the key entirely when empty so the
/// settings table doesn't accumulate empty-vec blobs.
async fn save_relay_outbox(&self, entries: Vec<NoteInfo>) -> Result<(), ClientError> {
let key = String::from(NOTE_TRANSPORT_OUTBOX_KEY);
let domain = SettingDomain::client(NOTE_TRANSPORT_SETTING_DOMAIN);
let key = String::from(NOTE_TRANSPORT_OUTBOX_SETTING);
if entries.is_empty() {
self.store.remove_setting(key).await.map_err(ClientError::StoreError)?;
self.store.remove_setting(&domain, key).await.map_err(ClientError::StoreError)?;
return Ok(());
}
let bytes = entries.to_bytes();
self.store.set_setting(key, bytes).await.map_err(ClientError::StoreError)
self.store
.set_setting(&domain, key, bytes)
.await
.map_err(ClientError::StoreError)
}

/// The set of tracked tags eligible for history backfill.
Expand Down Expand Up @@ -284,9 +292,10 @@ impl<AUTH> Client<AUTH> {
/// tracked tag as new only triggers a one-off backfill, which dedupes, whereas leaving
/// unreadable bytes in place would fail every subsequent sync.
async fn load_covered_tags(&self) -> Result<BTreeSet<NoteTag>, ClientError> {
let domain = SettingDomain::client(NOTE_TRANSPORT_SETTING_DOMAIN);
let bytes = self
.store
.get_setting(String::from(NOTE_TRANSPORT_COVERED_TAGS_KEY))
.get_setting(&domain, String::from(NOTE_TRANSPORT_COVERED_TAGS_SETTING))
.await
.map_err(ClientError::StoreError)?;
let Some(bytes) = bytes else {
Expand All @@ -297,7 +306,7 @@ impl<AUTH> Client<AUTH> {
Err(err) => {
tracing::warn!(?err, "dropping unreadable covered-tags set; resetting to empty");
self.store
.remove_setting(String::from(NOTE_TRANSPORT_COVERED_TAGS_KEY))
.remove_setting(&domain, String::from(NOTE_TRANSPORT_COVERED_TAGS_SETTING))
.await
.map_err(ClientError::StoreError)?;
Ok(BTreeSet::new())
Expand All @@ -308,13 +317,14 @@ impl<AUTH> Client<AUTH> {
/// Persist the covered-tags set, removing the key entirely when empty so the settings table
/// doesn't accumulate empty-vec blobs.
async fn save_covered_tags(&self, tags: &BTreeSet<NoteTag>) -> Result<(), ClientError> {
let key = String::from(NOTE_TRANSPORT_COVERED_TAGS_KEY);
let domain = SettingDomain::client(NOTE_TRANSPORT_SETTING_DOMAIN);
let key = String::from(NOTE_TRANSPORT_COVERED_TAGS_SETTING);
if tags.is_empty() {
self.store.remove_setting(key).await.map_err(ClientError::StoreError)?;
self.store.remove_setting(&domain, key).await.map_err(ClientError::StoreError)?;
return Ok(());
}
self.store
.set_setting(key, tags.to_bytes())
.set_setting(&domain, key, tags.to_bytes())
.await
.map_err(ClientError::StoreError)
}
Expand Down Expand Up @@ -363,11 +373,11 @@ where
/// The global transport cursor is shared across all tracked tags and only moves forward, so a
/// tag that starts being tracked late never sees its notes that already sit below the cursor.
/// This diffs the tracked `User`/`Account` tags (see [`Self::backfill_candidate_tags`]) against
/// the persisted covered set (see [`NOTE_TRANSPORT_COVERED_TAGS_KEY`]) and drains each newly
/// tracked tag from the start, fetching only that tag's own history rather than re-scanning
/// everything. Tags no longer tracked are dropped from the covered set so a later re-add
/// backfills again instead of resuming from a stale mark. Imports dedupe, so the overlap with
/// the steady-state stream is harmless.
/// the persisted covered set (see [`NOTE_TRANSPORT_COVERED_TAGS_SETTING`]) and drains each
/// newly tracked tag from the start, fetching only that tag's own history rather than
/// re-scanning everything. Tags no longer tracked are dropped from the covered set so a
/// later re-add backfills again instead of resuming from a stale mark. Imports dedupe, so
/// the overlap with the steady-state stream is harmless.
///
/// At most [`Self::MAX_BACKFILL_TAGS_PER_SYNC`] tags are backfilled per call; any remainder
/// stays uncovered and is picked up on the next sync. Returns the ids of notes imported here.
Expand Down
Loading
Loading