use miden_protocol::{Felt, Word};
use super::bytes32::packed_felts_to_bytes32;
use super::deposit_intent::ForeignChainAddress;
use super::domain::CircleDomain;
use super::error::EncodingError;
use crate::note::xreserve_burn::XUsdcBurnAttachment;
#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
pub struct XReserveBurnItems {
pub dest_domain: CircleDomain,
pub dest_recipient: ForeignChainAddress,
}
impl XReserveBurnItems {
pub fn encode(&self) -> [Word; XUsdcBurnAttachment::NUM_WORDS] {
let recipient = self.dest_recipient.to_packed_felts();
let mut items = [Word::empty(); XUsdcBurnAttachment::NUM_WORDS];
items[0][0] = Felt::from(self.dest_domain);
items[1].as_mut_slice().copy_from_slice(&recipient[0..4]);
items[2].as_mut_slice().copy_from_slice(&recipient[4..8]);
items
}
pub fn decode(words: &[Word]) -> Result<Self, EncodingError> {
let [domain_word, recipient_low, recipient_high] = words else {
return Err(EncodingError::BurnItemsMalformed);
};
let [domain, padding @ ..] = domain_word.into_elements();
if padding.iter().any(|element| *element != Felt::ZERO) {
return Err(EncodingError::BurnItemsMalformed);
}
let dest_domain = u32::try_from(domain.as_canonical_u64())
.map(CircleDomain::new)
.map_err(|_| EncodingError::BurnItemsMalformed)?;
let mut recipient = [Felt::ZERO; 2 * Word::NUM_ELEMENTS];
recipient[0..4].copy_from_slice(recipient_low.as_elements());
recipient[4..8].copy_from_slice(recipient_high.as_elements());
let dest_recipient = packed_felts_to_bytes32(&recipient)
.map(ForeignChainAddress::new)
.map_err(|_| EncodingError::BurnItemsMalformed)?;
Ok(Self { dest_domain, dest_recipient })
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use rstest::rstest;
use super::*;
use crate::vectors::load;
#[test]
fn tv_bn_1_round_trip() {
let v = load();
let accept: Vec<_> = v.families.bn.iter().filter(|x| x.kind == "accept").collect();
assert!(!accept.is_empty(), "bn accept vectors present");
for vec in accept {
let x = vec.expected_struct();
let encoded = x.encode();
assert_eq!(
encoded.as_slice(),
vec.items_words(),
"{}: encode matches the golden layout",
vec.id
);
assert_eq!(
XReserveBurnItems::decode(&encoded).expect("round-trip decode"),
x,
"{}: decode∘encode",
vec.id
);
assert_eq!(
XReserveBurnItems::decode(&vec.items_words()).expect("golden decode"),
x,
"{}: decode golden words",
vec.id
);
}
}
#[test]
fn tv_bn_1_random_round_trip() -> anyhow::Result<()> {
const NUM_CASES: usize = 256;
for _ in 0..NUM_CASES {
let items = XReserveBurnItems::builder()
.dest_domain(CircleDomain::new(rand::random()))
.dest_recipient(ForeignChainAddress::new(rand::random()))
.build();
assert_eq!(XReserveBurnItems::decode(&items.encode())?, items);
let mut payload = [Word::empty(); XUsdcBurnAttachment::NUM_WORDS];
payload[0][0] = Felt::from(rand::random::<u32>());
for element in payload[1..].iter_mut().flat_map(|word| word.as_mut_slice()) {
*element = Felt::from(rand::random::<u32>());
}
assert_eq!(XReserveBurnItems::decode(&payload)?.encode(), payload);
}
Ok(())
}
#[test]
fn tv_bn_2_destination_in_items() {
let v = load();
for vec in v.families.bn.iter().filter(|x| x.kind == "accept") {
let items = vec.expected_struct().encode();
let golden = vec.items_words();
assert_eq!(items[0], golden[0], "{}: destDomain in word 0", vec.id);
assert_eq!(&items[1..3], &golden[1..3], "{}: destRecipient in words 1 and 2", vec.id);
}
}
#[rstest]
#[case("bn-rej-len-short")]
#[case("bn-rej-len-long")]
#[case("bn-rej-domain-padding-not-zero")]
#[case("bn-rej-domain-over-u32")]
#[case("bn-rej-recipient-limb-not-u32")]
fn tv_bn_4_malformed_burn_items(#[case] id: &str) {
let vec = load()
.families
.bn
.iter()
.find(|x| x.id == id)
.unwrap_or_else(|| panic!("vector {id} present"));
assert_matches!(
XReserveBurnItems::decode(&vec.items_words()),
Err(EncodingError::BurnItemsMalformed),
"{id}",
);
}
}