1use miden_protocol::account::{AccountId, StorageMapKey};
36use miden_protocol::asset::AssetAmount;
37use miden_protocol::utils::bytes_to_packed_u32_elements;
38use miden_protocol::utils::serde::{
39 ByteReader,
40 ByteWriter,
41 Deserializable,
42 DeserializationError,
43 Serializable,
44 SliceReader,
45};
46use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
47use miden_standards::interop::eth::{EthAmount, EthEmbeddedAccountId};
48
49use super::account_id::EthEmbeddedAccountIdExt;
50use super::amount::uint256_to_asset_amount;
51use super::bytes32::{bytes32_to_packed_felts, bytes32_to_storage_map_key};
52use super::domain::CircleDomain;
53use super::error::EncodingError;
54use crate::note::xreserve_mint::XUSDC_MINT_TRANSPORT_HOOK_DATA_MAX_LEN;
55
56pub const BYTES_PER_PACKED_FELT: usize = 4;
61
62pub const BYTES32_LEN: usize = 32;
65pub const ACCOUNT_ID_BYTES: usize = 16;
66pub const ASSET_AMOUNT_BYTES: usize = AssetAmount::SERIALIZED_SIZE;
67
68pub const BYTES32_PACKED_LIMBS: usize = BYTES32_LEN / BYTES_PER_PACKED_FELT;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum DepositIntentField {
73 Magic,
74 Version,
75 Amount,
76 RemoteDomain,
77 RemoteToken,
78 RemoteRecipient,
79 LocalToken,
80 LocalDepositor,
81 MaxFee,
82 Nonce,
83 HookDataLen,
84 HookData,
85}
86
87impl DepositIntentField {
88 pub const fn offset(self) -> usize {
90 match self {
91 Self::Magic => 0,
92 Self::Version => 4,
93 Self::Amount => 8,
94 Self::RemoteDomain => 40,
95 Self::RemoteToken => 44,
96 Self::RemoteRecipient => 76,
97 Self::LocalToken => 108,
98 Self::LocalDepositor => 140,
99 Self::MaxFee => 172,
100 Self::Nonce => 204,
101 Self::HookDataLen => 236,
102 Self::HookData => 240,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct DepositNonce([u8; BYTES32_LEN]);
117
118impl DepositNonce {
119 pub const fn new(bytes: [u8; BYTES32_LEN]) -> Self {
121 Self(bytes)
122 }
123
124 pub const fn as_bytes(&self) -> &[u8; BYTES32_LEN] {
126 &self.0
127 }
128
129 pub fn to_packed_felts(&self) -> [Felt; BYTES32_PACKED_LIMBS] {
131 bytes32_to_packed_felts(&self.0)
132 }
133
134 pub fn to_word(&self) -> Word {
136 self.to_storage_map_key().as_word()
137 }
138
139 pub fn to_storage_map_key(&self) -> StorageMapKey {
141 bytes32_to_storage_map_key(&self.0)
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct ForeignChainAddress([u8; BYTES32_LEN]);
153
154impl ForeignChainAddress {
155 pub const fn new(bytes: [u8; BYTES32_LEN]) -> Self {
157 Self(bytes)
158 }
159
160 pub const fn as_bytes(&self) -> &[u8; BYTES32_LEN] {
162 &self.0
163 }
164
165 pub fn to_packed_felts(&self) -> [Felt; BYTES32_PACKED_LIMBS] {
167 bytes32_to_packed_felts(&self.0)
168 }
169}
170
171#[derive(Debug, Clone, Default, PartialEq, Eq)]
180pub struct HookData(Vec<u8>);
181
182impl HookData {
183 pub const MAX_LEN: usize = XUSDC_MINT_TRANSPORT_HOOK_DATA_MAX_LEN;
185
186 pub fn new(bytes: Vec<u8>) -> Result<Self, EncodingError> {
192 if bytes.len() > Self::MAX_LEN {
193 return Err(EncodingError::HookDataTooLarge);
194 }
195 Ok(Self(bytes))
196 }
197
198 pub fn as_bytes(&self) -> &[u8] {
200 &self.0
201 }
202
203 pub fn len_u32(&self) -> u32 {
205 u32::try_from(self.0.len()).expect("hook data length is bounded by HookData::MAX_LEN")
206 }
207
208 pub fn is_empty(&self) -> bool {
210 self.0.is_empty()
211 }
212
213 pub fn to_packed_elements(&self) -> Vec<Felt> {
216 bytes_to_packed_u32_elements(&self.0)
217 }
218}
219
220const _: () = assert!(
224 DepositIntent::HEADER_NUM_FELTS + HookData::MAX_LEN / BYTES_PER_PACKED_FELT
225 <= MAX_NOTE_STORAGE_ITEMS,
226 "the rebuilt preimage of a max-length hookData must fit the protocol's note-storage limit"
227);
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, bon::Builder)]
238pub struct DepositIntentHeader {
239 amount: AssetAmount,
240 remote_domain: CircleDomain,
241 remote_token: AccountId,
242 remote_recipient: AccountId,
243 local_token: ForeignChainAddress,
244 local_depositor: ForeignChainAddress,
245 max_fee: AssetAmount,
246 nonce: DepositNonce,
247}
248
249impl DepositIntentHeader {
250 pub const MAGIC: u32 = 0x5a2e_0acd;
252 pub const VERSION: u32 = 1;
253
254 pub const SERIALIZED_SIZE: usize = DepositIntentField::HookDataLen.offset();
257
258 pub fn amount(&self) -> AssetAmount {
260 self.amount
261 }
262
263 pub fn remote_domain(&self) -> CircleDomain {
265 self.remote_domain
266 }
267
268 pub fn remote_token(&self) -> AccountId {
270 self.remote_token
271 }
272
273 pub fn remote_recipient(&self) -> AccountId {
275 self.remote_recipient
276 }
277
278 pub fn local_token(&self) -> ForeignChainAddress {
280 self.local_token
281 }
282
283 pub fn local_depositor(&self) -> ForeignChainAddress {
285 self.local_depositor
286 }
287
288 pub fn max_fee(&self) -> AssetAmount {
290 self.max_fee
291 }
292
293 pub fn nonce(&self) -> DepositNonce {
295 self.nonce
296 }
297
298 fn read<R: ByteReader>(source: &mut R) -> Result<Self, EncodingError> {
300 let bytes: [u8; Self::SERIALIZED_SIZE] =
302 source.read_array().map_err(|_| EncodingError::TruncatedHeader)?;
303
304 if be_u32(&bytes, DepositIntentField::Magic) != Self::MAGIC {
305 return Err(EncodingError::BadMagic);
306 }
307 if be_u32(&bytes, DepositIntentField::Version) != Self::VERSION {
308 return Err(EncodingError::BadVersion);
309 }
310
311 let amount = non_zero_bytes32(&bytes, DepositIntentField::Amount)?;
315 let local_token = non_zero_bytes32(&bytes, DepositIntentField::LocalToken)?;
316 let local_depositor = non_zero_bytes32(&bytes, DepositIntentField::LocalDepositor)?;
317
318 Ok(Self {
319 amount: reduce(amount, DepositIntentField::Amount)?,
320 remote_domain: CircleDomain::new(be_u32(&bytes, DepositIntentField::RemoteDomain)),
321 remote_token: account_id(&bytes, DepositIntentField::RemoteToken)?,
322 remote_recipient: account_id(&bytes, DepositIntentField::RemoteRecipient)?,
323 local_token: ForeignChainAddress::new(local_token),
324 local_depositor: ForeignChainAddress::new(local_depositor),
325 max_fee: reduce(
326 bytes32_at(&bytes, DepositIntentField::MaxFee),
327 DepositIntentField::MaxFee,
328 )?,
329 nonce: DepositNonce::new(bytes32_at(&bytes, DepositIntentField::Nonce)),
330 })
331 }
332}
333
334impl Serializable for DepositIntentHeader {
335 fn write_into<W: ByteWriter>(&self, target: &mut W) {
338 let mut bytes = [0u8; Self::SERIALIZED_SIZE];
339
340 write_u32(&mut bytes, DepositIntentField::Magic, Self::MAGIC);
341 write_u32(&mut bytes, DepositIntentField::Version, Self::VERSION);
342 write_bytes32(&mut bytes, DepositIntentField::Amount, &widen(self.amount));
343 write_u32(&mut bytes, DepositIntentField::RemoteDomain, self.remote_domain.as_u32());
344 write_bytes32(
345 &mut bytes,
346 DepositIntentField::RemoteToken,
347 &EthEmbeddedAccountId::from_account_id(self.remote_token).to_bytes32(),
348 );
349 write_bytes32(
350 &mut bytes,
351 DepositIntentField::RemoteRecipient,
352 &EthEmbeddedAccountId::from_account_id(self.remote_recipient).to_bytes32(),
353 );
354 write_bytes32(&mut bytes, DepositIntentField::LocalToken, self.local_token.as_bytes());
355 write_bytes32(
356 &mut bytes,
357 DepositIntentField::LocalDepositor,
358 self.local_depositor.as_bytes(),
359 );
360 write_bytes32(&mut bytes, DepositIntentField::MaxFee, &widen(self.max_fee));
361 write_bytes32(&mut bytes, DepositIntentField::Nonce, self.nonce.as_bytes());
362
363 target.write_bytes(&bytes);
364 }
365
366 fn get_size_hint(&self) -> usize {
367 Self::SERIALIZED_SIZE
368 }
369}
370
371impl Deserializable for DepositIntentHeader {
372 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
373 Self::read(source).map_err(DeserializationError::from)
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct DepositIntent {
386 header: DepositIntentHeader,
387 hook_data: HookData,
388}
389
390impl DepositIntent {
391 pub const HEADER_SIZE: usize = DepositIntentField::HookData.offset();
393
394 pub const HEADER_NUM_FELTS: usize = Self::HEADER_SIZE / BYTES_PER_PACKED_FELT;
397
398 pub fn new(header: DepositIntentHeader, hook_data: HookData) -> Self {
400 Self { header, hook_data }
401 }
402
403 pub fn header(&self) -> &DepositIntentHeader {
405 &self.header
406 }
407
408 pub fn hook_data(&self) -> &HookData {
410 &self.hook_data
411 }
412
413 pub fn preimage_felt_len(&self) -> usize {
415 Self::HEADER_NUM_FELTS + self.hook_data.as_bytes().len().div_ceil(BYTES_PER_PACKED_FELT)
416 }
417
418 pub fn to_preimage_felts(&self) -> Vec<Felt> {
421 bytes_to_packed_u32_elements(&self.to_bytes())
422 }
423
424 fn read<R: ByteReader>(source: &mut R) -> Result<Self, EncodingError> {
429 let header = DepositIntentHeader::read(source)?;
430
431 let hook_data_len = source
434 .read_array::<BYTES_PER_PACKED_FELT>()
435 .map(u32::from_be_bytes)
436 .map_err(|_| EncodingError::TruncatedHeader)? as usize;
437
438 if hook_data_len > HookData::MAX_LEN {
441 return Err(EncodingError::HookDataTooLarge);
442 }
443 let hook_data =
444 source.read_vec(hook_data_len).map_err(|_| EncodingError::LengthMismatch)?;
445 if source.has_more_bytes() {
446 return Err(EncodingError::LengthMismatch);
447 }
448
449 Ok(Self::new(header, HookData::new(hook_data)?))
450 }
451}
452
453impl Serializable for DepositIntent {
454 fn write_into<W: ByteWriter>(&self, target: &mut W) {
457 self.header.write_into(target);
458 target.write_bytes(&self.hook_data.len_u32().to_be_bytes());
459 target.write_bytes(self.hook_data.as_bytes());
460 }
461
462 fn get_size_hint(&self) -> usize {
463 Self::HEADER_SIZE + self.hook_data.as_bytes().len()
464 }
465}
466
467impl Deserializable for DepositIntent {
468 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
469 Self::read(source).map_err(DeserializationError::from)
470 }
471}
472
473impl TryFrom<&[u8]> for DepositIntent {
474 type Error = EncodingError;
475
476 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
479 Self::read(&mut SliceReader::new(bytes))
480 }
481}
482
483fn be_u32(bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE], field: DepositIntentField) -> u32 {
488 let offset = field.offset();
489 u32::from_be_bytes(
490 bytes[offset..offset + BYTES_PER_PACKED_FELT].try_into().expect("4-byte window"),
491 )
492}
493
494fn bytes32_at(
496 bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
497 field: DepositIntentField,
498) -> [u8; BYTES32_LEN] {
499 let offset = field.offset();
500 bytes[offset..offset + BYTES32_LEN].try_into().expect("32-byte window")
501}
502
503fn non_zero_bytes32(
505 bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
506 field: DepositIntentField,
507) -> Result<[u8; BYTES32_LEN], EncodingError> {
508 let value = bytes32_at(bytes, field);
509 if value.iter().all(|byte| *byte == 0) {
510 return Err(EncodingError::ZeroField { field });
511 }
512 Ok(value)
513}
514
515fn reduce(
519 value: [u8; BYTES32_LEN],
520 field: DepositIntentField,
521) -> Result<AssetAmount, EncodingError> {
522 uint256_to_asset_amount(EthAmount::new(value))
523 .map_err(|_| EncodingError::FieldNotAssetAmount { field })
524}
525
526fn widen(amount: AssetAmount) -> [u8; BYTES32_LEN] {
529 let mut bytes = [0u8; BYTES32_LEN];
530 bytes[BYTES32_LEN - ASSET_AMOUNT_BYTES..].copy_from_slice(&amount.as_u64().to_be_bytes());
531 bytes
532}
533
534fn account_id(
537 bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
538 field: DepositIntentField,
539) -> Result<AccountId, EncodingError> {
540 Ok(EthEmbeddedAccountId::try_from_bytes32(bytes32_at(bytes, field))?.into_account_id())
541}
542
543fn write_u32(
545 out: &mut [u8; DepositIntentHeader::SERIALIZED_SIZE],
546 field: DepositIntentField,
547 value: u32,
548) {
549 let offset = field.offset();
550 out[offset..offset + BYTES_PER_PACKED_FELT].copy_from_slice(&value.to_be_bytes());
551}
552
553fn write_bytes32(
557 out: &mut [u8; DepositIntentHeader::SERIALIZED_SIZE],
558 field: DepositIntentField,
559 value: &[u8],
560) {
561 let start = field.offset() + BYTES32_LEN - value.len();
562 out[start..start + value.len()].copy_from_slice(value);
563}
564
565#[cfg(test)]
569mod tests {
570 use assert_matches::assert_matches;
571 use rstest::rstest;
572
573 use super::*;
574 use crate::vectors::load;
575
576 #[test]
584 fn tv_di_1_positive_parse() {
585 let v = load();
586 for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
587 let bytes = vec.bytes();
588 let intent = DepositIntent::try_from(bytes.as_slice())
589 .unwrap_or_else(|e| panic!("vector {}: must parse, got {e}", vec.id));
590 let header = intent.header();
591 let f = vec.fields.as_ref().expect("accept vector carries fields");
592
593 assert_eq!(header.remote_domain(), f.remote_domain, "vector {}: remoteDomain", vec.id);
594 assert_eq!(
595 intent.hook_data().len_u32(),
596 f.hook_data_len,
597 "vector {}: hookDataLen",
598 vec.id
599 );
600 assert_eq!(u64::from(header.amount()), f.amount, "vector {}: amount", vec.id);
601 assert_eq!(u64::from(header.max_fee()), f.max_fee, "vector {}: maxFee", vec.id);
602
603 let written = intent.to_bytes();
604 for (name, field, label) in [
605 ("amount", DepositIntentField::Amount, "amount"),
606 ("remote_token", DepositIntentField::RemoteToken, "remoteToken"),
607 ("remote_recipient", DepositIntentField::RemoteRecipient, "remoteRecipient"),
608 ("local_token", DepositIntentField::LocalToken, "localToken"),
609 ("local_depositor", DepositIntentField::LocalDepositor, "localDepositor"),
610 ("max_fee", DepositIntentField::MaxFee, "maxFee"),
611 ("nonce", DepositIntentField::Nonce, "nonce"),
612 ] {
613 let offset = field.offset();
614 assert_eq!(
615 &written[offset..offset + BYTES32_LEN],
616 &f.bytes32(name),
617 "vector {}: {label}",
618 vec.id
619 );
620 }
621 }
622 }
623
624 #[test]
628 fn round_trip_is_byte_exact() {
629 let v = load();
630 for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
631 let bytes = vec.bytes();
632 let intent = DepositIntent::try_from(bytes.as_slice()).expect("accept vector decodes");
633 assert_eq!(intent.to_bytes(), bytes, "vector {}: round trip", vec.id);
634 }
635 }
636
637 #[rstest]
640 #[case::tv_di_2_bad_magic("di-rej-bad-magic")]
641 #[case::tv_di_3_bad_version("di-rej-bad-version")]
642 #[case::tv_di_4_zero_amount("di-rej-zero-amount")]
643 #[case::tv_di_5_zero_local_token("di-rej-zero-local-token")]
644 #[case::tv_di_5_zero_local_depositor("di-rej-zero-local-depositor")]
645 #[case::tv_di_6_length_mismatch("di-rej-length-mismatch")]
646 #[case::tv_di_6_truncated_header("di-rej-truncated")]
647 fn tv_di_rejects(#[case] id: &str) {
648 let v = load();
649 let vec = v.families.di.iter().find(|v| v.id == id).expect("vector present");
650 let result = DepositIntent::try_from(vec.bytes().as_slice());
651 match vec.expected_variant.as_deref() {
652 Some("BadMagic") => assert_matches!(result, Err(EncodingError::BadMagic), "{id}"),
653 Some("BadVersion") => assert_matches!(result, Err(EncodingError::BadVersion), "{id}"),
654 Some("ZeroField:Amount") => assert_matches!(
655 result,
656 Err(EncodingError::ZeroField { field: DepositIntentField::Amount }),
657 "{id}"
658 ),
659 Some("ZeroField:LocalToken") => assert_matches!(
660 result,
661 Err(EncodingError::ZeroField { field: DepositIntentField::LocalToken }),
662 "{id}"
663 ),
664 Some("ZeroField:LocalDepositor") => assert_matches!(
665 result,
666 Err(EncodingError::ZeroField {
667 field: DepositIntentField::LocalDepositor
668 }),
669 "{id}"
670 ),
671 Some("LengthMismatch") => {
672 assert_matches!(result, Err(EncodingError::LengthMismatch), "{id}")
673 },
674 Some("TruncatedHeader") => {
675 assert_matches!(result, Err(EncodingError::TruncatedHeader), "{id}")
676 },
677 other => panic!("vector {id}: unexpected expected_variant {other:?}"),
678 }
679 }
680
681 #[test]
684 fn a_payload_short_of_the_fixed_prefix_is_truncated() {
685 let v = load();
686 let vec = v.families.di.iter().find(|v| v.id == "di-pos-empty-hookdata").expect("vector");
687 let bytes = vec.bytes();
688 for len in [DepositIntentHeader::SERIALIZED_SIZE, DepositIntent::HEADER_SIZE - 1] {
689 assert_matches!(
690 DepositIntent::try_from(&bytes[..len]),
691 Err(EncodingError::TruncatedHeader),
692 "a {len}-byte payload stops inside the fixed prefix"
693 );
694 }
695 }
696
697 #[test]
701 fn trailing_bytes_reject() {
702 let v = load();
703 let vec = v.families.di.iter().find(|v| v.id == "di-pos-hookdata").expect("vector");
704 let mut bytes = vec.bytes();
705 bytes.push(0x00);
706 assert_matches!(
707 DepositIntent::try_from(bytes.as_slice()),
708 Err(EncodingError::LengthMismatch)
709 );
710 assert_eq!(
711 DepositIntent::read_from_bytes(&bytes)
712 .expect_err("the standard reader refuses the trailing byte too")
713 .to_string(),
714 DeserializationError::from(EncodingError::LengthMismatch).to_string()
715 );
716 }
717
718 #[test]
722 fn tv_di_7_sixty_felts_and_1024_bound() {
723 let v = load();
724 for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
725 let intent =
726 DepositIntent::try_from(vec.bytes().as_slice()).expect("accept vector decodes");
727 let felts = intent.to_preimage_felts();
728 let expected = vec.preimage_values();
729 assert_eq!(felts.len(), expected.len(), "vector {}: total felts", vec.id);
730 assert_eq!(
731 felts.len(),
732 intent.preimage_felt_len(),
733 "vector {}: derived felt length",
734 vec.id
735 );
736 assert_eq!(
737 &felts[..DepositIntent::HEADER_NUM_FELTS],
738 &expected[..DepositIntent::HEADER_NUM_FELTS],
739 "vector {}: 60-felt header",
740 vec.id
741 );
742 assert_eq!(felts, expected, "vector {}: full preimage", vec.id);
743 assert!(felts.len() <= MAX_NOTE_STORAGE_ITEMS, "vector {}: NoteStorage bound", vec.id);
744 }
745 let overflow = v
746 .families
747 .di
748 .iter()
749 .find(|v| v.id == "di-rej-hookdata-overflow")
750 .expect("vector");
751 assert_matches!(
752 DepositIntent::try_from(overflow.bytes().as_slice()),
753 Err(EncodingError::HookDataTooLarge),
754 "hookData past the 1024-felt bound must reject"
755 );
756 }
757
758 #[test]
761 fn tv_di_8_input_immutable() {
762 let v = load();
763 let vec = v.families.di.iter().find(|v| v.id == "di-pos-hookdata").expect("vector");
764 let bytes = vec.bytes();
765 let before = bytes.clone();
766 let _ = DepositIntent::try_from(bytes.as_slice());
767 assert_eq!(bytes, before, "input must be unchanged by parsing");
768 }
769
770 #[test]
772 fn tv_di_9_offsets_table() {
773 let expected: [(DepositIntentField, usize); 12] = [
774 (DepositIntentField::Magic, 0),
775 (DepositIntentField::Version, 4),
776 (DepositIntentField::Amount, 8),
777 (DepositIntentField::RemoteDomain, 40),
778 (DepositIntentField::RemoteToken, 44),
779 (DepositIntentField::RemoteRecipient, 76),
780 (DepositIntentField::LocalToken, 108),
781 (DepositIntentField::LocalDepositor, 140),
782 (DepositIntentField::MaxFee, 172),
783 (DepositIntentField::Nonce, 204),
784 (DepositIntentField::HookDataLen, 236),
785 (DepositIntentField::HookData, 240),
786 ];
787 for (field, off) in expected {
788 assert_eq!(field.offset(), off, "wire offset of {field:?}");
789 }
790 }
791
792 #[test]
795 fn hook_data_bound_is_enforced_at_construction() {
796 assert_matches!(
797 HookData::new(vec![0u8; HookData::MAX_LEN + 1]),
798 Err(EncodingError::HookDataTooLarge)
799 );
800 assert!(HookData::new(vec![0u8; HookData::MAX_LEN]).is_ok());
801 }
802}