pub mod errors;
pub mod generated;
#[cfg(feature = "tonic")]
pub mod grpc;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use futures::Stream;
use miden_protocol::address::Address;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::{
Note,
NoteDetails,
NoteDetailsCommitment,
NoteFile,
NoteHeader,
NoteId,
NoteTag,
};
use miden_protocol::utils::serde::Serializable;
use miden_tx::auth::TransactionAuthenticator;
use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
SliceReader,
};
pub use self::errors::NoteTransportError;
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";
pub const NOTE_TRANSPORT_OUTBOX_KEY: &str = "note_transport_outbox";
impl<AUTH> Client<AUTH> {
pub fn is_note_transport_enabled(&self) -> bool {
self.note_transport_api.is_some()
}
pub(crate) fn get_note_transport_api(
&self,
) -> Result<Arc<dyn NoteTransportClient>, NoteTransportError> {
self.note_transport_api.clone().ok_or(NoteTransportError::Disabled)
}
#[deprecated(
since = "0.15.2",
note = "use `Client::send_private_note_with_block_hint` to relay a block hint for deterministic delivery"
)]
pub async fn send_private_note(
&mut self,
note: Note,
address: &Address,
) -> Result<(), ClientError> {
self.relay_private_note(note, address, None).await
}
pub async fn send_private_note_with_block_hint(
&mut self,
note: Note,
address: &Address,
block_hint: BlockNumber,
) -> Result<(), ClientError> {
self.relay_private_note(note, address, Some(block_hint)).await
}
async fn relay_private_note(
&self,
note: Note,
_address: &Address,
block_hint: Option<BlockNumber>,
) -> Result<(), ClientError> {
let api = self.get_note_transport_api()?;
let header = *note.header();
let note_id = header.id();
let details = NoteDetails::from(note);
let details_bytes = details.to_bytes();
let entry = NoteInfo {
header,
details_bytes: details_bytes.clone(),
block_hint,
};
let mut outbox = self.load_relay_outbox().await?;
outbox.retain(|e| e.header.id() != note_id);
outbox.push(entry);
self.save_relay_outbox(outbox).await?;
match block_hint {
Some(block_hint) => {
api.send_note_with_block_hint(header, details_bytes, block_hint).await?;
},
None => {
api.send_note(header, details_bytes).await?;
},
}
let mut outbox = self.load_relay_outbox().await?;
outbox.retain(|e| e.header.id() != note_id);
self.save_relay_outbox(outbox).await?;
Ok(())
}
pub async fn flush_relay_outbox(&self) -> Result<(), ClientError> {
let api = self.get_note_transport_api()?;
let entries = self.load_relay_outbox().await?;
if entries.is_empty() {
return Ok(());
}
let mut remaining = Vec::new();
let mut last_err: Option<NoteTransportError> = None;
for entry in entries {
let relayed = match entry.block_hint {
Some(block_hint) => {
api.send_note_with_block_hint(
entry.header,
entry.details_bytes.clone(),
block_hint,
)
.await
},
None => api.send_note(entry.header, entry.details_bytes.clone()).await,
};
match relayed {
Ok(()) => {},
Err(err) => {
tracing::warn!(?err, "relay-outbox entry retry failed; will retry next sync");
remaining.push(entry);
last_err = Some(err);
},
}
}
self.save_relay_outbox(remaining).await?;
if let Some(err) = last_err {
return Err(err.into());
}
Ok(())
}
async fn load_relay_outbox(&self) -> Result<Vec<NoteInfo>, ClientError> {
let bytes = self
.store
.get_setting(String::from(NOTE_TRANSPORT_OUTBOX_KEY))
.await
.map_err(ClientError::StoreError)?;
let Some(bytes) = bytes else {
return Ok(Vec::new());
};
match Vec::<NoteInfo>::read_from_bytes(&bytes) {
Ok(entries) => Ok(entries),
Err(err) => {
if let Ok(legacy) = Vec::<LegacyNoteInfo>::read_from_bytes(&bytes) {
return Ok(legacy
.into_iter()
.map(|note| NoteInfo::new(note.header, note.details_bytes))
.collect());
}
tracing::warn!(?err, "dropping unreadable relay outbox; resetting to empty");
self.store
.remove_setting(String::from(NOTE_TRANSPORT_OUTBOX_KEY))
.await
.map_err(ClientError::StoreError)?;
Ok(Vec::new())
},
}
}
async fn save_relay_outbox(&self, entries: Vec<NoteInfo>) -> Result<(), ClientError> {
let key = String::from(NOTE_TRANSPORT_OUTBOX_KEY);
if entries.is_empty() {
return self.store.remove_setting(key).await.map_err(ClientError::StoreError);
}
let bytes = entries.to_bytes();
self.store.set_setting(key, bytes).await.map_err(ClientError::StoreError)
}
}
impl<AUTH> Client<AUTH>
where
AUTH: TransactionAuthenticator + Sync + 'static,
{
pub async fn fetch_private_notes(&mut self) -> Result<(), ClientError> {
let note_tags: Vec<NoteTag> =
self.store.get_unique_note_tags().await?.into_iter().collect();
let cursor = self.store.get_note_transport_cursor().await?;
let (_, new_cursor) = self.fetch_transport_notes(cursor, ¬e_tags).await?;
self.store.update_note_transport_cursor(new_cursor).await?;
Ok(())
}
pub async fn fetch_all_private_notes(&mut self) -> Result<(), ClientError> {
const MAX_ITERATIONS: usize = 1_000;
let note_tags: Vec<NoteTag> =
self.store.get_unique_note_tags().await?.into_iter().collect();
let stored_cursor = self.store.get_note_transport_cursor().await?;
let mut cursor = NoteTransportCursor::init();
for _ in 0..MAX_ITERATIONS {
let (_, new_cursor) = self.fetch_transport_notes(cursor, ¬e_tags).await?;
if new_cursor <= cursor {
let final_cursor = core::cmp::max(cursor, stored_cursor);
self.store.update_note_transport_cursor(final_cursor).await?;
return Ok(());
}
cursor = new_cursor;
}
Err(ClientError::NoteTransportError(NoteTransportError::PaginationDidNotTerminate(
MAX_ITERATIONS,
)))
}
pub(crate) async fn fetch_transport_notes(
&mut self,
cursor: NoteTransportCursor,
tags: &[NoteTag],
) -> Result<(Vec<NoteId>, NoteTransportCursor), ClientError> {
const NOTE_LOOKBACK_BLOCKS: u32 = 20;
let mut notes = Vec::new();
let (note_infos, rcursor) =
self.get_note_transport_api()?.fetch_notes(tags, cursor).await?;
for note_info in ¬e_infos {
let note = rejoin_note(¬e_info.header, ¬e_info.details_bytes)?;
notes.push((note, note_info.block_hint));
}
let sync_height = self.get_sync_height().await?;
let fallback_after_block_num =
BlockNumber::from(sync_height.as_u32().saturating_sub(NOTE_LOOKBACK_BLOCKS));
let id_by_commitment: BTreeMap<NoteDetailsCommitment, NoteId> =
notes.iter().map(|(note, _)| (note.details_commitment(), note.id())).collect();
let mut note_requests = Vec::with_capacity(notes.len());
for (note, block_hint) in notes {
let tag = note.metadata().tag();
let after_block_num = block_hint.unwrap_or(fallback_after_block_num);
let note_file = NoteFile::NoteDetails {
details: note.into(),
after_block_num,
tag: Some(tag),
};
note_requests.push(note_file);
}
let imported_commitments = self.import_notes(¬e_requests).await?;
let imported_ids = imported_commitments
.into_iter()
.filter_map(|commitment| id_by_commitment.get(&commitment).copied())
.collect();
Ok((imported_ids, rcursor))
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub struct NoteTransportCursor(u64);
pub struct NoteTransportUpdate {
pub cursor: NoteTransportCursor,
pub notes: Vec<Note>,
}
impl NoteTransportCursor {
pub fn new(value: u64) -> Self {
Self(value)
}
pub fn init() -> Self {
Self::new(0)
}
pub fn value(&self) -> u64 {
self.0
}
}
impl From<u64> for NoteTransportCursor {
fn from(value: u64) -> Self {
Self::new(value)
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait NoteTransportClient: Send + Sync {
async fn send_note(
&self,
header: NoteHeader,
details: Vec<u8>,
) -> Result<(), NoteTransportError>;
async fn send_note_with_block_hint(
&self,
header: NoteHeader,
details: Vec<u8>,
_block_hint: BlockNumber,
) -> Result<(), NoteTransportError> {
self.send_note(header, details).await
}
async fn fetch_notes(
&self,
tag: &[NoteTag],
cursor: NoteTransportCursor,
) -> Result<(Vec<NoteInfo>, NoteTransportCursor), NoteTransportError>;
async fn stream_notes(
&self,
tag: NoteTag,
cursor: NoteTransportCursor,
) -> Result<Box<dyn NoteStream>, NoteTransportError>;
}
pub trait NoteStream:
Stream<Item = Result<Vec<NoteInfo>, NoteTransportError>> + Send + Unpin
{
}
#[derive(Debug, Clone)]
pub struct NoteInfo {
pub header: NoteHeader,
pub details_bytes: Vec<u8>,
pub block_hint: Option<BlockNumber>,
}
impl NoteInfo {
pub fn new(header: NoteHeader, details_bytes: Vec<u8>) -> Self {
Self { header, details_bytes, block_hint: None }
}
}
impl Serializable for NoteInfo {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.header.write_into(target);
self.details_bytes.write_into(target);
self.block_hint.write_into(target);
}
}
impl Deserializable for NoteInfo {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let header = NoteHeader::read_from(source)?;
let details_bytes = Vec::<u8>::read_from(source)?;
let block_hint = Option::<BlockNumber>::read_from(source)?;
Ok(NoteInfo { header, details_bytes, block_hint })
}
}
struct LegacyNoteInfo {
header: NoteHeader,
details_bytes: Vec<u8>,
}
impl Deserializable for LegacyNoteInfo {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let header = NoteHeader::read_from(source)?;
let details_bytes = Vec::<u8>::read_from(source)?;
Ok(LegacyNoteInfo { header, details_bytes })
}
}
impl Serializable for NoteTransportCursor {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.0.write_into(target);
}
}
impl Deserializable for NoteTransportCursor {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let value = u64::read_from(source)?;
Ok(Self::new(value))
}
}
fn rejoin_note(header: &NoteHeader, details_bytes: &[u8]) -> Result<Note, DeserializationError> {
let mut reader = SliceReader::new(details_bytes);
let details = NoteDetails::read_from(&mut reader)?;
let partial_metadata = *header.metadata().partial_metadata();
Ok(Note::new(
details.assets().clone(),
partial_metadata,
details.recipient().clone(),
))
}