use std::collections::BTreeMap;
use std::sync::Arc;
use miden_client::note::NoteFile;
use miden_client::rpc::NodeRpcClient;
use miden_client::store::{InputNoteRecord, NoteFilter as StoreNoteFilter};
use miden_client::transaction::{NoteArgs, TransactionRequest, TransactionRequestBuilder};
use miden_protocol::note::{Note, NoteId, NoteInclusionProof};
use miden_protocol::{Felt, Word};
use miden_standards::account::auth::MultisigAuthArgs;
use super::TransactionRequestBuilderExt;
use crate::MidenSdkClient;
use crate::error::{MultisigError, Result};
pub(crate) async fn fetch_notes_from_store(
client: &MidenSdkClient,
note_ids: &[NoteId],
) -> Result<Vec<Note>> {
let mut notes: Vec<Note> = Vec::with_capacity(note_ids.len());
for note_id in note_ids {
let input_note_record = client
.get_input_note(*note_id)
.await
.map_err(|e| MultisigError::miden_client_with_context("failed to fetch note", e))?
.ok_or(MultisigError::LegacyConsumeNotesNoteMissing { note_id: *note_id })?;
let note: Note = input_note_record.try_into().map_err(|e| {
MultisigError::InvalidConfig(format!("failed to convert note record to note: {:?}", e))
})?;
notes.push(note);
}
Ok(notes)
}
pub fn build_consume_notes_transaction_request_from_notes<I>(
notes: Vec<Note>,
auth_args: &MultisigAuthArgs,
signature_advice: I,
) -> Result<TransactionRequest>
where
I: IntoIterator<Item = (Word, Vec<Felt>)>,
{
if notes.is_empty() {
return Err(MultisigError::InvalidConfig(
"no notes specified for consumption".to_string(),
));
}
let note_and_args: Vec<(Note, Option<NoteArgs>)> =
notes.into_iter().map(|n| (n, None)).collect();
let mut builder = TransactionRequestBuilder::new().input_notes(note_and_args);
for (key, values) in signature_advice {
builder = builder.extend_advice_map([(key, values)]);
}
builder.multisig_auth_args(auth_args).build().map_err(|e| {
MultisigError::TransactionExecution(format!("failed to build transaction request: {}", e))
})
}
pub async fn build_consume_notes_transaction_request<I>(
client: &MidenSdkClient,
note_ids: Vec<NoteId>,
auth_args: &MultisigAuthArgs,
signature_advice: I,
) -> Result<TransactionRequest>
where
I: IntoIterator<Item = (Word, Vec<Felt>)>,
{
if note_ids.is_empty() {
return Err(MultisigError::InvalidConfig(
"no notes specified for consumption".to_string(),
));
}
let notes = fetch_notes_from_store(client, ¬e_ids).await?;
build_consume_notes_transaction_request_from_notes(notes, auth_args, signature_advice)
}
pub(crate) async fn ensure_notes_authenticated(
client: &mut MidenSdkClient,
node_rpc: &Arc<dyn NodeRpcClient>,
notes: &[Note],
) -> Result<()> {
let pending = unauthenticated_notes(client, notes).await?;
if pending.is_empty() {
return Ok(());
}
let ids: Vec<NoteId> = pending.iter().map(Note::id).collect();
let fetched = node_rpc.get_notes_by_id(&ids).await.map_err(|e| {
MultisigError::ConsumeNoteNotAuthenticated {
note_id: ids[0],
reason: format!("failed to fetch inclusion proofs from the node: {e}"),
}
})?;
let mut proofs: BTreeMap<NoteId, NoteInclusionProof> = fetched
.iter()
.map(|f| (f.id(), f.inclusion_proof().clone()))
.collect();
for note in pending {
let note_id = note.id();
let Some(proof) = proofs.remove(¬e_id) else {
return Err(MultisigError::ConsumeNoteNotAuthenticated {
note_id,
reason: "the node has no inclusion proof for it (not committed on chain yet)"
.to_string(),
});
};
client
.import_notes(&[NoteFile::Committed { note, proof }])
.await
.map_err(|e| MultisigError::ConsumeNoteNotAuthenticated {
note_id,
reason: format!("failed to import it with its inclusion proof: {e}"),
})?;
}
if !unauthenticated_notes(client, notes).await?.is_empty() {
client.sync_state().await.map_err(|e| {
MultisigError::miden_client_with_context(
"failed to sync while authenticating consume-notes input notes",
e,
)
})?;
}
if let Some(still) = unauthenticated_notes(client, notes).await?.first() {
return Err(MultisigError::ConsumeNoteNotAuthenticated {
note_id: still.id(),
reason: "its inclusion proof was imported but the local store could not \
verify it against the chain even after a sync"
.to_string(),
});
}
Ok(())
}
async fn unauthenticated_notes(client: &MidenSdkClient, notes: &[Note]) -> Result<Vec<Note>> {
let records: BTreeMap<NoteId, InputNoteRecord> = client
.get_input_notes(StoreNoteFilter::List(notes.iter().map(Note::id).collect()))
.await
.map_err(|e| MultisigError::miden_client_with_context("failed to read local notes", e))?
.into_iter()
.filter_map(|record| record.id().map(|id| (id, record)))
.collect();
Ok(notes
.iter()
.filter(|note| {
!records
.get(¬e.id())
.is_some_and(InputNoteRecord::is_authenticated)
})
.cloned()
.collect())
}