use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use alloc::{format, vec};
use miden_protocol::note::{Note, NoteDetails, NoteId, NoteInclusionProof};
use miden_protocol::{Felt, Word};
use miden_standards::note::PswapNote;
use super::errors::PswapLineageError;
use super::lineage::{
PswapLineageFilter,
PswapLineageRecord,
PswapLineageRoundUpdate,
PswapLineageState,
};
use crate::store::input_note_states::{CommittedNoteState, UnverifiedNoteState};
use crate::store::{InputNoteRecord, NoteFilter, SettingMutation, Store, StoreError};
use crate::sync::{NoteTagRecord, NoteTagSource};
use crate::utils::{Deserializable, Serializable, bytes_to_hex_string};
const ORDER_PREFIX: &str = "pswap/order/";
const TIP_PREFIX: &str = "pswap/tip/";
fn order_key(order_id: Felt) -> String {
format!(
"{ORDER_PREFIX}{}",
bytes_to_hex_string(order_id.as_canonical_u64().to_le_bytes())
)
}
fn tip_key(tip: NoteId) -> String {
format!("{TIP_PREFIX}{}", tip.as_word())
}
pub(crate) async fn put_lineage(
store: &Arc<dyn Store>,
record: &PswapLineageRecord,
) -> Result<(), StoreError> {
store
.apply_settings_mutations(vec![
SettingMutation::Set {
key: order_key(record.order_id()),
value: record.to_bytes(),
},
SettingMutation::Set {
key: tip_key(record.current_tip_note_id),
value: record.order_id().to_bytes(),
},
])
.await
}
pub(crate) async fn get_lineage(
store: &Arc<dyn Store>,
order_id: Felt,
) -> Result<Option<PswapLineageRecord>, StoreError> {
let Some(bytes) = store.get_setting(order_key(order_id)).await? else {
return Ok(None);
};
let record = PswapLineageRecord::read_from_bytes(&bytes)
.map_err(StoreError::DataDeserializationError)?;
Ok(Some(record))
}
pub(crate) async fn resolve_order_by_tip(
store: &Arc<dyn Store>,
tip: NoteId,
) -> Result<Option<Felt>, StoreError> {
let Some(bytes) = store.get_setting(tip_key(tip)).await? else {
return Ok(None);
};
let order_id = Felt::read_from_bytes(&bytes).map_err(StoreError::DataDeserializationError)?;
Ok(Some(order_id))
}
pub(crate) async fn get_original_pswap(
store: &Arc<dyn Store>,
original_note_id: NoteId,
) -> Result<PswapNote, PswapLineageError> {
let record = store
.get_output_notes(NoteFilter::List(vec![original_note_id]))
.await?
.into_iter()
.next()
.ok_or(PswapLineageError::OriginalNoteUnavailable(original_note_id))?;
let note: Note = record
.try_into()
.map_err(|_| PswapLineageError::OriginalNoteUnavailable(original_note_id))?;
PswapNote::try_from(¬e)
.map_err(|_| PswapLineageError::OriginalNoteUnavailable(original_note_id))
}
pub(crate) async fn list_lineages(
store: &Arc<dyn Store>,
filter: PswapLineageFilter,
) -> Result<Vec<PswapLineageRecord>, StoreError> {
let mut out = Vec::new();
for key in store.list_setting_keys().await? {
if !key.starts_with(ORDER_PREFIX) {
continue;
}
let Some(bytes) = store.get_setting(key).await? else {
continue;
};
let record = PswapLineageRecord::read_from_bytes(&bytes)
.map_err(StoreError::DataDeserializationError)?;
let keep = match &filter {
PswapLineageFilter::All => true,
PswapLineageFilter::Active => record.state == PswapLineageState::Active,
PswapLineageFilter::ByCreator(creator) => record.creator_account_id() == *creator,
};
if keep {
out.push(record);
}
}
Ok(out)
}
pub(crate) async fn apply_round(
store: &Arc<dyn Store>,
update: &PswapLineageRoundUpdate,
) -> Result<(), StoreError> {
let record = get_lineage(store, update.order_id).await?.ok_or_else(|| {
StoreError::DatabaseError(format!(
"apply_round: no lineage for order_id {}",
update.order_id
))
})?;
if update.round_depth != record.current_depth + 1 {
return Err(StoreError::DatabaseError(format!(
"apply_round: round_depth {} for order_id {} does not advance by 1 \
(current_depth {}); refusing to corrupt the reconstruction chain",
update.round_depth, update.order_id, record.current_depth,
)));
}
let at_block_note_root = update.at_block_note_root;
if let Some((payback_note, inclusion_proof)) = &update.payback {
upsert_round_note(store, payback_note, inclusion_proof, at_block_note_root).await?;
}
if let Some((remainder_note, inclusion_proof)) = &update.remainder {
upsert_round_note(store, remainder_note, inclusion_proof, at_block_note_root).await?;
}
let old_tip = record.current_tip_note_id;
let new_record = record.advance(update);
let mut mutations = vec![
SettingMutation::Set {
key: order_key(update.order_id),
value: new_record.to_bytes(),
},
SettingMutation::Remove { key: tip_key(old_tip) },
];
if update.state == PswapLineageState::Active
&& let Some(new_tip) = update.tip_note_id
{
mutations.push(SettingMutation::Set {
key: tip_key(new_tip),
value: update.order_id.to_bytes(),
});
}
store.apply_settings_mutations(mutations).await?;
if matches!(update.state, PswapLineageState::FullyFilled | PswapLineageState::Reclaimed) {
let pswap =
get_original_pswap(store, new_record.original_note_id).await.map_err(|err| {
StoreError::DatabaseError(format!(
"apply_round: cannot recover the depth-0 note to remove the asset-pair tag for \
order_id {}: {err}",
update.order_id
))
})?;
store
.remove_note_tag(NoteTagRecord {
tag: PswapNote::create_tag(
pswap.note_type(),
pswap.offered_asset(),
pswap.storage().requested_asset(),
),
source: NoteTagSource::Subscription(new_record.original_note_id.as_word()),
})
.await?;
}
Ok(())
}
async fn upsert_round_note(
store: &Arc<dyn Store>,
note: &Note,
inclusion_proof: &NoteInclusionProof,
at_block_note_root: Option<Word>,
) -> Result<(), StoreError> {
let note_id = note.id();
if !store.get_input_notes(NoteFilter::List(vec![note_id])).await?.is_empty() {
return Ok(());
}
let metadata = *note.metadata();
let details = NoteDetails::from(note.clone());
let attachments = note.attachments().clone();
let state = match at_block_note_root {
Some(note_root) => CommittedNoteState {
inclusion_proof: inclusion_proof.clone(),
metadata,
block_note_root: note_root,
}
.into(),
None => UnverifiedNoteState {
metadata,
inclusion_proof: inclusion_proof.clone(),
}
.into(),
};
store
.upsert_input_notes(&[InputNoteRecord::new(details, attachments, None, state)])
.await
}
#[cfg(test)]
mod tests {
use miden_protocol::Word;
use super::*;
fn felt(value: u64) -> Felt {
Felt::new(value).unwrap()
}
fn note_id(value: u64) -> NoteId {
let f = felt(value);
NoteId::from_raw(Word::from([f, f, f, f]))
}
#[test]
fn order_key_carries_order_prefix() {
assert!(order_key(felt(1)).starts_with(ORDER_PREFIX));
}
#[test]
fn tip_key_carries_tip_prefix() {
assert!(tip_key(note_id(1)).starts_with(TIP_PREFIX));
}
#[test]
fn key_families_are_prefix_isolated() {
assert!(!TIP_PREFIX.starts_with(ORDER_PREFIX));
assert!(!ORDER_PREFIX.starts_with(TIP_PREFIX));
assert!(!tip_key(note_id(1)).starts_with(ORDER_PREFIX));
assert!(!order_key(felt(1)).starts_with(TIP_PREFIX));
}
#[test]
fn keys_are_deterministic_and_injective() {
let order_a = order_key(felt(7));
let order_b = order_key(felt(7));
assert_eq!(order_a, order_b);
assert_ne!(order_key(felt(1)), order_key(felt(2)));
let tip_a = tip_key(note_id(7));
let tip_b = tip_key(note_id(7));
assert_eq!(tip_a, tip_b);
assert_ne!(tip_key(note_id(1)), tip_key(note_id(2)));
}
}