Skip to content
Merged
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 @@ -26,6 +26,7 @@
* Fixed MMR reconstruction code and fixed how block authentication paths are adjusted ([#1633](https://github.com/0xMiden/miden-client/pull/1633)).
* Added WebClient bindings and RPC helpers for additional account, note, and validation workflows ([#1638](https://github.com/0xMiden/miden-client/pull/1638)).
* [BREAKING] Modified JS binding for `AccountComponent::compile` which now takes an `AccountComponentCode` built with the newly added binding `CodeBuilder::compile_account_component_code` ([#1627](https://github.com/0xMiden/miden-client/pull/1627)).
* [BREAKING] Simplified the `NoteScreener` API, removing `NoteRelevance` in favor of `NoteConsumptionStatus`; exposed JS bindings for consumption check results ([#1630](https://github.com/0xMiden/miden-client/pull/1630)).
* [BREAKING] Replaced `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)).

## 0.12.5 (2025-12-01)
Expand Down
20 changes: 19 additions & 1 deletion bin/miden-cli/src/commands/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use miden_client::auth::TransactionAuthenticator;
use miden_client::note::{
Note,
NoteConsumability,
NoteConsumptionStatus,
NoteInputs,
NoteMetadata,
WellKnownNote,
Expand Down Expand Up @@ -395,14 +396,31 @@ where
table.add_row(vec![
note.id().to_hex(),
relevance.0.to_string(),
relevance.1.to_string(),
note_consumption_status_type(&relevance.1),
]);
}
}

println!("{table}");
}

fn note_consumption_status_type(note_consumption_status: &NoteConsumptionStatus) -> String {
match note_consumption_status {
NoteConsumptionStatus::Consumable => "Consumable".to_string(),
NoteConsumptionStatus::ConsumableAfter(block_number) => {
format!("Consumable after block {block_number}")
},
NoteConsumptionStatus::ConsumableWithAuthorization => {
"Consumable with authorization".to_string()
},
NoteConsumptionStatus::UnconsumableConditions => {
"Unconsumable due to conditions".to_string()
},
NoteConsumptionStatus::NeverConsumable(error) => format!("Never consumable: {error}"),
}
.to_string()
}

fn note_record_type(note_record_metadata: Option<&NoteMetadata>) -> String {
match note_record_metadata {
Some(metadata) => match metadata.note_type() {
Expand Down
3 changes: 2 additions & 1 deletion crates/rust-client/src/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ pub use miden_protocol::note::{
pub use miden_protocol::transaction::ToInputNoteCommitments;
pub use miden_standards::note::utils::{build_p2id_recipient, build_swap_tag};
pub use miden_standards::note::{
NoteConsumptionStatus,
WellKnownNote,
create_p2id_note,
create_p2ide_note,
create_swap_note,
};
pub use note_screener::{NoteConsumability, NoteRelevance, NoteScreener, NoteScreenerError};
pub use note_screener::{NoteConsumability, NoteScreener, NoteScreenerError};
pub use note_update_tracker::{
InputNoteUpdate,
NoteUpdateTracker,
Expand Down
91 changes: 11 additions & 80 deletions crates/rust-client/src/note/note_screener.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;

use async_trait::async_trait;
use miden_protocol::account::{Account, AccountId};
use miden_protocol::note::{Note, NoteId};
use miden_protocol::{AccountError, AssetError};
use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt};
use miden_standards::note::{NoteConsumptionStatus, WellKnownNote};
use miden_standards::note::NoteConsumptionStatus;
use miden_tx::auth::TransactionAuthenticator;
use miden_tx::{NoteCheckerError, NoteConsumptionChecker, TransactionExecutor};
use thiserror::Error;
Expand All @@ -20,29 +19,11 @@ use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
use crate::sync::{NoteUpdateAction, OnNoteReceived};
use crate::transaction::{InputNote, TransactionRequestBuilder, TransactionRequestError};

/// Describes the relevance of a note based on the screening.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum NoteRelevance {
/// The note can be consumed in the client's current block.
Now,
/// The note can be consumed after the block with the specified number.
After(u32),
}

/// Represents the consumability of a note by a specific account.
///
/// The tuple contains the account ID that may consume the note and the moment it will become
/// relevant.
pub type NoteConsumability = (AccountId, NoteRelevance);

impl fmt::Display for NoteRelevance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NoteRelevance::Now => write!(f, "Now"),
NoteRelevance::After(height) => write!(f, "After block {height}"),
}
}
}
pub type NoteConsumability = (AccountId, NoteConsumptionStatus);

/// Provides functionality for testing whether a note is relevant to the client or not.
///
Expand All @@ -69,7 +50,8 @@ where
/// Returns a vector of tuples describing the relevance of the provided note to the
/// accounts monitored by this screener.
///
/// If relevance can't be determined, the screener defaults to setting the note as consumable.
/// The relevance is determined by [`NoteConsumptionChecker::can_consume`] and is based on
/// current conditions (for example, it takes the latest block in the client as reference).
pub async fn check_relevance(
&self,
note: &Note,
Expand All @@ -85,24 +67,12 @@ where
.try_into()
.map_err(|_| NoteScreenerError::AccountDataNotFound(id))?;

match self.check_standard_consumability(&account, note).await {
Ok(Some(relevance)) => {
match self.check_standard_consumability(&account, note).await? {
NoteConsumptionStatus::NeverConsumable(_)
| NoteConsumptionStatus::UnconsumableConditions => {},
relevance => {
note_relevances.push((id, relevance));
},
Ok(None) => {
// The note might be consumable after a certain block height if the note is
// p2ide
let script_root = note.script().root();

if script_root == WellKnownNote::P2IDE.script_root()
&& let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
{
note_relevances.push((id, relevance));
}
},
// If an error occurs while checking consumability, we count it as not relevant for
// that account
Err(_) => {},
}
}

Expand All @@ -111,11 +81,11 @@ where

/// Tries to execute a standard consume transaction to check if the note is consumable by the
/// account.
async fn check_standard_consumability(
pub async fn check_standard_consumability(
&self,
account: &Account,
note: &Note,
) -> Result<Option<NoteRelevance>, NoteScreenerError> {
) -> Result<NoteConsumptionStatus, NoteScreenerError> {
let transaction_request =
TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;

Expand All @@ -142,46 +112,7 @@ where
)
.await?;

let result = match note_consumption_check {
NoteConsumptionStatus::ConsumableAfter(block_number) => {
Some(NoteRelevance::After(block_number.as_u32()))
},
NoteConsumptionStatus::Consumable
| NoteConsumptionStatus::ConsumableWithAuthorization => Some(NoteRelevance::Now),
// NOTE: NoteConsumptionStatus::UnconsumableConditions means that state-related context
// does not allow for consumption, so don't keep for now. In the next
// version, we should be more careful about this
NoteConsumptionStatus::UnconsumableConditions
| NoteConsumptionStatus::NeverConsumable(_) => None,
};
Ok(result)
}

/// Special relevance check for P2IDE notes. It checks if the sender account can consume and
/// recall the note.
fn check_p2ide_recall_consumability(
note: &Note,
account_id: &AccountId,
) -> Result<Option<NoteRelevance>, NoteScreenerError> {
let note_inputs = note.inputs().values();
// TODO: this needs to be removed (see note screener refactor issue)

if note_inputs.len() != 4 {
return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
}

let recall_height_felt = note_inputs[2];

let sender = note.metadata().sender();
let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
})?;

if sender == *account_id {
Ok(Some(NoteRelevance::After(recall_height)))
} else {
Ok(None)
}
Ok(note_consumption_check)
}
}

Expand Down
23 changes: 17 additions & 6 deletions crates/testing/miden-client-tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use miden_client::auth::{
};
use miden_client::builder::ClientBuilder;
use miden_client::keystore::FilesystemKeyStore;
use miden_client::note::{BlockNumber, NoteId, NoteRelevance};
use miden_client::note::{BlockNumber, NoteId};
use miden_client::rpc::{ACCOUNT_ID_LIMIT, NOTE_TAG_LIMIT, NodeRpcClient};
use miden_client::store::input_note_states::ConsumedAuthenticatedLocalNoteState;
use miden_client::store::{
Expand Down Expand Up @@ -108,7 +108,7 @@ use miden_protocol::{EMPTY_WORD, Felt, ONE, Word, ZERO};
use miden_standards::account::faucets::BasicFungibleFaucet;
use miden_standards::account::interface::AccountInterfaceError;
use miden_standards::account::wallets::BasicWallet;
use miden_standards::note::{WellKnownNote, utils};
use miden_standards::note::{NoteConsumptionStatus, WellKnownNote, utils};
use miden_standards::testing::mock_account::MockAccountExt;
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{MockChain, MockChainBuilder, TxContextInput};
Expand Down Expand Up @@ -1503,20 +1503,31 @@ async fn get_consumable_notes() {

// Check that the note is only consumable after block 100 for the account that sent the
// transaction
let from_account_relevance = relevant_accounts
let from_account_relevance = &relevant_accounts
.iter()
.find(|relevance| relevance.0 == from_account_id)
.unwrap()
.1;
assert_eq!(from_account_relevance, NoteRelevance::After(100));
match from_account_relevance {
NoteConsumptionStatus::ConsumableAfter(value) => {
assert_eq!(value, &(100u32.into()));
},
_ => panic!("Unexpected NoteConsumptionStatus"),
}

// Check that the note is always consumable for the account that received the transaction
let to_account_relevance = relevant_accounts
let to_account_relevance = &relevant_accounts
.iter()
.find(|relevance| relevance.0 == to_account_id)
.unwrap()
.1;
assert_eq!(to_account_relevance, NoteRelevance::Now);

match to_account_relevance {
NoteConsumptionStatus::Consumable
| NoteConsumptionStatus::ConsumableAfter(..)
| NoteConsumptionStatus::ConsumableWithAuthorization => {},
_ => panic!("Unexpected NoteConsumptionStatus"),
}
}

#[tokio::test]
Expand Down
1 change: 1 addition & 0 deletions crates/web-client/js/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export {
NoteAndArgsArray,
NoteAssets,
NoteConsumability,
NoteConsumptionStatus,
NoteDetails,
NoteDetailsAndTag,
NoteDetailsAndTagArray,
Expand Down
Loading