use miden_protocol::account::{AccountId, StorageMapKey};
use miden_protocol::asset::AssetAmount;
use miden_protocol::utils::bytes_to_packed_u32_elements;
use miden_protocol::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
SliceReader,
};
use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
use miden_standards::interop::eth::{EthAmount, EthEmbeddedAccountId};
use super::account_id::EthEmbeddedAccountIdExt;
use super::amount::uint256_to_asset_amount;
use super::bytes32::{bytes32_to_packed_felts, bytes32_to_storage_map_key};
use super::domain::CircleDomain;
use super::error::EncodingError;
use crate::note::xreserve_mint::XUSDC_MINT_TRANSPORT_HOOK_DATA_MAX_LEN;
pub const BYTES_PER_PACKED_FELT: usize = 4;
pub const BYTES32_LEN: usize = 32;
pub const ACCOUNT_ID_BYTES: usize = 16;
pub const ASSET_AMOUNT_BYTES: usize = AssetAmount::SERIALIZED_SIZE;
pub const BYTES32_PACKED_LIMBS: usize = BYTES32_LEN / BYTES_PER_PACKED_FELT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepositIntentField {
Magic,
Version,
Amount,
RemoteDomain,
RemoteToken,
RemoteRecipient,
LocalToken,
LocalDepositor,
MaxFee,
Nonce,
HookDataLen,
HookData,
}
impl DepositIntentField {
pub const fn offset(self) -> usize {
match self {
Self::Magic => 0,
Self::Version => 4,
Self::Amount => 8,
Self::RemoteDomain => 40,
Self::RemoteToken => 44,
Self::RemoteRecipient => 76,
Self::LocalToken => 108,
Self::LocalDepositor => 140,
Self::MaxFee => 172,
Self::Nonce => 204,
Self::HookDataLen => 236,
Self::HookData => 240,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DepositNonce([u8; BYTES32_LEN]);
impl DepositNonce {
pub const fn new(bytes: [u8; BYTES32_LEN]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; BYTES32_LEN] {
&self.0
}
pub fn to_packed_felts(&self) -> [Felt; BYTES32_PACKED_LIMBS] {
bytes32_to_packed_felts(&self.0)
}
pub fn to_word(&self) -> Word {
self.to_storage_map_key().as_word()
}
pub fn to_storage_map_key(&self) -> StorageMapKey {
bytes32_to_storage_map_key(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ForeignChainAddress([u8; BYTES32_LEN]);
impl ForeignChainAddress {
pub const fn new(bytes: [u8; BYTES32_LEN]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; BYTES32_LEN] {
&self.0
}
pub fn to_packed_felts(&self) -> [Felt; BYTES32_PACKED_LIMBS] {
bytes32_to_packed_felts(&self.0)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HookData(Vec<u8>);
impl HookData {
pub const MAX_LEN: usize = XUSDC_MINT_TRANSPORT_HOOK_DATA_MAX_LEN;
pub fn new(bytes: Vec<u8>) -> Result<Self, EncodingError> {
if bytes.len() > Self::MAX_LEN {
return Err(EncodingError::HookDataTooLarge);
}
Ok(Self(bytes))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn len_u32(&self) -> u32 {
u32::try_from(self.0.len()).expect("hook data length is bounded by HookData::MAX_LEN")
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn to_packed_elements(&self) -> Vec<Felt> {
bytes_to_packed_u32_elements(&self.0)
}
}
const _: () = assert!(
DepositIntent::HEADER_NUM_FELTS + HookData::MAX_LEN / BYTES_PER_PACKED_FELT
<= MAX_NOTE_STORAGE_ITEMS,
"the rebuilt preimage of a max-length hookData must fit the protocol's note-storage limit"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, bon::Builder)]
pub struct DepositIntentHeader {
amount: AssetAmount,
remote_domain: CircleDomain,
remote_token: AccountId,
remote_recipient: AccountId,
local_token: ForeignChainAddress,
local_depositor: ForeignChainAddress,
max_fee: AssetAmount,
nonce: DepositNonce,
}
impl DepositIntentHeader {
pub const MAGIC: u32 = 0x5a2e_0acd;
pub const VERSION: u32 = 1;
pub const SERIALIZED_SIZE: usize = DepositIntentField::HookDataLen.offset();
pub fn amount(&self) -> AssetAmount {
self.amount
}
pub fn remote_domain(&self) -> CircleDomain {
self.remote_domain
}
pub fn remote_token(&self) -> AccountId {
self.remote_token
}
pub fn remote_recipient(&self) -> AccountId {
self.remote_recipient
}
pub fn local_token(&self) -> ForeignChainAddress {
self.local_token
}
pub fn local_depositor(&self) -> ForeignChainAddress {
self.local_depositor
}
pub fn max_fee(&self) -> AssetAmount {
self.max_fee
}
pub fn nonce(&self) -> DepositNonce {
self.nonce
}
fn read<R: ByteReader>(source: &mut R) -> Result<Self, EncodingError> {
let bytes: [u8; Self::SERIALIZED_SIZE] =
source.read_array().map_err(|_| EncodingError::TruncatedHeader)?;
if be_u32(&bytes, DepositIntentField::Magic) != Self::MAGIC {
return Err(EncodingError::BadMagic);
}
if be_u32(&bytes, DepositIntentField::Version) != Self::VERSION {
return Err(EncodingError::BadVersion);
}
let amount = non_zero_bytes32(&bytes, DepositIntentField::Amount)?;
let local_token = non_zero_bytes32(&bytes, DepositIntentField::LocalToken)?;
let local_depositor = non_zero_bytes32(&bytes, DepositIntentField::LocalDepositor)?;
Ok(Self {
amount: reduce(amount, DepositIntentField::Amount)?,
remote_domain: CircleDomain::new(be_u32(&bytes, DepositIntentField::RemoteDomain)),
remote_token: account_id(&bytes, DepositIntentField::RemoteToken)?,
remote_recipient: account_id(&bytes, DepositIntentField::RemoteRecipient)?,
local_token: ForeignChainAddress::new(local_token),
local_depositor: ForeignChainAddress::new(local_depositor),
max_fee: reduce(
bytes32_at(&bytes, DepositIntentField::MaxFee),
DepositIntentField::MaxFee,
)?,
nonce: DepositNonce::new(bytes32_at(&bytes, DepositIntentField::Nonce)),
})
}
}
impl Serializable for DepositIntentHeader {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let mut bytes = [0u8; Self::SERIALIZED_SIZE];
write_u32(&mut bytes, DepositIntentField::Magic, Self::MAGIC);
write_u32(&mut bytes, DepositIntentField::Version, Self::VERSION);
write_bytes32(&mut bytes, DepositIntentField::Amount, &widen(self.amount));
write_u32(&mut bytes, DepositIntentField::RemoteDomain, self.remote_domain.as_u32());
write_bytes32(
&mut bytes,
DepositIntentField::RemoteToken,
&EthEmbeddedAccountId::from_account_id(self.remote_token).to_bytes32(),
);
write_bytes32(
&mut bytes,
DepositIntentField::RemoteRecipient,
&EthEmbeddedAccountId::from_account_id(self.remote_recipient).to_bytes32(),
);
write_bytes32(&mut bytes, DepositIntentField::LocalToken, self.local_token.as_bytes());
write_bytes32(
&mut bytes,
DepositIntentField::LocalDepositor,
self.local_depositor.as_bytes(),
);
write_bytes32(&mut bytes, DepositIntentField::MaxFee, &widen(self.max_fee));
write_bytes32(&mut bytes, DepositIntentField::Nonce, self.nonce.as_bytes());
target.write_bytes(&bytes);
}
fn get_size_hint(&self) -> usize {
Self::SERIALIZED_SIZE
}
}
impl Deserializable for DepositIntentHeader {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
Self::read(source).map_err(DeserializationError::from)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepositIntent {
header: DepositIntentHeader,
hook_data: HookData,
}
impl DepositIntent {
pub const HEADER_SIZE: usize = DepositIntentField::HookData.offset();
pub const HEADER_NUM_FELTS: usize = Self::HEADER_SIZE / BYTES_PER_PACKED_FELT;
pub fn new(header: DepositIntentHeader, hook_data: HookData) -> Self {
Self { header, hook_data }
}
pub fn header(&self) -> &DepositIntentHeader {
&self.header
}
pub fn hook_data(&self) -> &HookData {
&self.hook_data
}
pub fn preimage_felt_len(&self) -> usize {
Self::HEADER_NUM_FELTS + self.hook_data.as_bytes().len().div_ceil(BYTES_PER_PACKED_FELT)
}
pub fn to_preimage_felts(&self) -> Vec<Felt> {
bytes_to_packed_u32_elements(&self.to_bytes())
}
fn read<R: ByteReader>(source: &mut R) -> Result<Self, EncodingError> {
let header = DepositIntentHeader::read(source)?;
let hook_data_len = source
.read_array::<BYTES_PER_PACKED_FELT>()
.map(u32::from_be_bytes)
.map_err(|_| EncodingError::TruncatedHeader)? as usize;
if hook_data_len > HookData::MAX_LEN {
return Err(EncodingError::HookDataTooLarge);
}
let hook_data =
source.read_vec(hook_data_len).map_err(|_| EncodingError::LengthMismatch)?;
if source.has_more_bytes() {
return Err(EncodingError::LengthMismatch);
}
Ok(Self::new(header, HookData::new(hook_data)?))
}
}
impl Serializable for DepositIntent {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.header.write_into(target);
target.write_bytes(&self.hook_data.len_u32().to_be_bytes());
target.write_bytes(self.hook_data.as_bytes());
}
fn get_size_hint(&self) -> usize {
Self::HEADER_SIZE + self.hook_data.as_bytes().len()
}
}
impl Deserializable for DepositIntent {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
Self::read(source).map_err(DeserializationError::from)
}
}
impl TryFrom<&[u8]> for DepositIntent {
type Error = EncodingError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::read(&mut SliceReader::new(bytes))
}
}
fn be_u32(bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE], field: DepositIntentField) -> u32 {
let offset = field.offset();
u32::from_be_bytes(
bytes[offset..offset + BYTES_PER_PACKED_FELT].try_into().expect("4-byte window"),
)
}
fn bytes32_at(
bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
field: DepositIntentField,
) -> [u8; BYTES32_LEN] {
let offset = field.offset();
bytes[offset..offset + BYTES32_LEN].try_into().expect("32-byte window")
}
fn non_zero_bytes32(
bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
field: DepositIntentField,
) -> Result<[u8; BYTES32_LEN], EncodingError> {
let value = bytes32_at(bytes, field);
if value.iter().all(|byte| *byte == 0) {
return Err(EncodingError::ZeroField { field });
}
Ok(value)
}
fn reduce(
value: [u8; BYTES32_LEN],
field: DepositIntentField,
) -> Result<AssetAmount, EncodingError> {
uint256_to_asset_amount(EthAmount::new(value))
.map_err(|_| EncodingError::FieldNotAssetAmount { field })
}
fn widen(amount: AssetAmount) -> [u8; BYTES32_LEN] {
let mut bytes = [0u8; BYTES32_LEN];
bytes[BYTES32_LEN - ASSET_AMOUNT_BYTES..].copy_from_slice(&amount.as_u64().to_be_bytes());
bytes
}
fn account_id(
bytes: &[u8; DepositIntentHeader::SERIALIZED_SIZE],
field: DepositIntentField,
) -> Result<AccountId, EncodingError> {
Ok(EthEmbeddedAccountId::try_from_bytes32(bytes32_at(bytes, field))?.into_account_id())
}
fn write_u32(
out: &mut [u8; DepositIntentHeader::SERIALIZED_SIZE],
field: DepositIntentField,
value: u32,
) {
let offset = field.offset();
out[offset..offset + BYTES_PER_PACKED_FELT].copy_from_slice(&value.to_be_bytes());
}
fn write_bytes32(
out: &mut [u8; DepositIntentHeader::SERIALIZED_SIZE],
field: DepositIntentField,
value: &[u8],
) {
let start = field.offset() + BYTES32_LEN - value.len();
out[start..start + value.len()].copy_from_slice(value);
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use rstest::rstest;
use super::*;
use crate::vectors::load;
#[test]
fn tv_di_1_positive_parse() {
let v = load();
for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
let bytes = vec.bytes();
let intent = DepositIntent::try_from(bytes.as_slice())
.unwrap_or_else(|e| panic!("vector {}: must parse, got {e}", vec.id));
let header = intent.header();
let f = vec.fields.as_ref().expect("accept vector carries fields");
assert_eq!(header.remote_domain(), f.remote_domain, "vector {}: remoteDomain", vec.id);
assert_eq!(
intent.hook_data().len_u32(),
f.hook_data_len,
"vector {}: hookDataLen",
vec.id
);
assert_eq!(u64::from(header.amount()), f.amount, "vector {}: amount", vec.id);
assert_eq!(u64::from(header.max_fee()), f.max_fee, "vector {}: maxFee", vec.id);
let written = intent.to_bytes();
for (name, field, label) in [
("amount", DepositIntentField::Amount, "amount"),
("remote_token", DepositIntentField::RemoteToken, "remoteToken"),
("remote_recipient", DepositIntentField::RemoteRecipient, "remoteRecipient"),
("local_token", DepositIntentField::LocalToken, "localToken"),
("local_depositor", DepositIntentField::LocalDepositor, "localDepositor"),
("max_fee", DepositIntentField::MaxFee, "maxFee"),
("nonce", DepositIntentField::Nonce, "nonce"),
] {
let offset = field.offset();
assert_eq!(
&written[offset..offset + BYTES32_LEN],
&f.bytes32(name),
"vector {}: {label}",
vec.id
);
}
}
}
#[test]
fn round_trip_is_byte_exact() {
let v = load();
for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
let bytes = vec.bytes();
let intent = DepositIntent::try_from(bytes.as_slice()).expect("accept vector decodes");
assert_eq!(intent.to_bytes(), bytes, "vector {}: round trip", vec.id);
}
}
#[rstest]
#[case::tv_di_2_bad_magic("di-rej-bad-magic")]
#[case::tv_di_3_bad_version("di-rej-bad-version")]
#[case::tv_di_4_zero_amount("di-rej-zero-amount")]
#[case::tv_di_5_zero_local_token("di-rej-zero-local-token")]
#[case::tv_di_5_zero_local_depositor("di-rej-zero-local-depositor")]
#[case::tv_di_6_length_mismatch("di-rej-length-mismatch")]
#[case::tv_di_6_truncated_header("di-rej-truncated")]
fn tv_di_rejects(#[case] id: &str) {
let v = load();
let vec = v.families.di.iter().find(|v| v.id == id).expect("vector present");
let result = DepositIntent::try_from(vec.bytes().as_slice());
match vec.expected_variant.as_deref() {
Some("BadMagic") => assert_matches!(result, Err(EncodingError::BadMagic), "{id}"),
Some("BadVersion") => assert_matches!(result, Err(EncodingError::BadVersion), "{id}"),
Some("ZeroField:Amount") => assert_matches!(
result,
Err(EncodingError::ZeroField { field: DepositIntentField::Amount }),
"{id}"
),
Some("ZeroField:LocalToken") => assert_matches!(
result,
Err(EncodingError::ZeroField { field: DepositIntentField::LocalToken }),
"{id}"
),
Some("ZeroField:LocalDepositor") => assert_matches!(
result,
Err(EncodingError::ZeroField {
field: DepositIntentField::LocalDepositor
}),
"{id}"
),
Some("LengthMismatch") => {
assert_matches!(result, Err(EncodingError::LengthMismatch), "{id}")
},
Some("TruncatedHeader") => {
assert_matches!(result, Err(EncodingError::TruncatedHeader), "{id}")
},
other => panic!("vector {id}: unexpected expected_variant {other:?}"),
}
}
#[test]
fn a_payload_short_of_the_fixed_prefix_is_truncated() {
let v = load();
let vec = v.families.di.iter().find(|v| v.id == "di-pos-empty-hookdata").expect("vector");
let bytes = vec.bytes();
for len in [DepositIntentHeader::SERIALIZED_SIZE, DepositIntent::HEADER_SIZE - 1] {
assert_matches!(
DepositIntent::try_from(&bytes[..len]),
Err(EncodingError::TruncatedHeader),
"a {len}-byte payload stops inside the fixed prefix"
);
}
}
#[test]
fn trailing_bytes_reject() {
let v = load();
let vec = v.families.di.iter().find(|v| v.id == "di-pos-hookdata").expect("vector");
let mut bytes = vec.bytes();
bytes.push(0x00);
assert_matches!(
DepositIntent::try_from(bytes.as_slice()),
Err(EncodingError::LengthMismatch)
);
assert_eq!(
DepositIntent::read_from_bytes(&bytes)
.expect_err("the standard reader refuses the trailing byte too")
.to_string(),
DeserializationError::from(EncodingError::LengthMismatch).to_string()
);
}
#[test]
fn tv_di_7_sixty_felts_and_1024_bound() {
let v = load();
for vec in v.families.di.iter().filter(|v| v.kind == "accept") {
let intent =
DepositIntent::try_from(vec.bytes().as_slice()).expect("accept vector decodes");
let felts = intent.to_preimage_felts();
let expected = vec.preimage_values();
assert_eq!(felts.len(), expected.len(), "vector {}: total felts", vec.id);
assert_eq!(
felts.len(),
intent.preimage_felt_len(),
"vector {}: derived felt length",
vec.id
);
assert_eq!(
&felts[..DepositIntent::HEADER_NUM_FELTS],
&expected[..DepositIntent::HEADER_NUM_FELTS],
"vector {}: 60-felt header",
vec.id
);
assert_eq!(felts, expected, "vector {}: full preimage", vec.id);
assert!(felts.len() <= MAX_NOTE_STORAGE_ITEMS, "vector {}: NoteStorage bound", vec.id);
}
let overflow = v
.families
.di
.iter()
.find(|v| v.id == "di-rej-hookdata-overflow")
.expect("vector");
assert_matches!(
DepositIntent::try_from(overflow.bytes().as_slice()),
Err(EncodingError::HookDataTooLarge),
"hookData past the 1024-felt bound must reject"
);
}
#[test]
fn tv_di_8_input_immutable() {
let v = load();
let vec = v.families.di.iter().find(|v| v.id == "di-pos-hookdata").expect("vector");
let bytes = vec.bytes();
let before = bytes.clone();
let _ = DepositIntent::try_from(bytes.as_slice());
assert_eq!(bytes, before, "input must be unchanged by parsing");
}
#[test]
fn tv_di_9_offsets_table() {
let expected: [(DepositIntentField, usize); 12] = [
(DepositIntentField::Magic, 0),
(DepositIntentField::Version, 4),
(DepositIntentField::Amount, 8),
(DepositIntentField::RemoteDomain, 40),
(DepositIntentField::RemoteToken, 44),
(DepositIntentField::RemoteRecipient, 76),
(DepositIntentField::LocalToken, 108),
(DepositIntentField::LocalDepositor, 140),
(DepositIntentField::MaxFee, 172),
(DepositIntentField::Nonce, 204),
(DepositIntentField::HookDataLen, 236),
(DepositIntentField::HookData, 240),
];
for (field, off) in expected {
assert_eq!(field.offset(), off, "wire offset of {field:?}");
}
}
#[test]
fn hook_data_bound_is_enforced_at_construction() {
assert_matches!(
HookData::new(vec![0u8; HookData::MAX_LEN + 1]),
Err(EncodingError::HookDataTooLarge)
);
assert!(HookData::new(vec![0u8; HookData::MAX_LEN]).is_ok());
}
}