use alloy_primitives::{Address, U256 as AlloyU256};
use crate::evm::abi::{SpendIntent, SpendSpec};
use crate::proto::v1;
#[derive(Debug, thiserror::Error)]
pub enum ProtoConversionError {
#[error("Missing field `{field}`")]
MissingField { field: &'static str },
#[error(
"Invalid EVM address length for field `{field}`: expected {expected} bytes, got {found}"
)]
InvalidEvmAddressLength {
field: &'static str,
expected: usize,
found: usize,
},
#[error("Invalid U256 byte length for field `{field}`: expected {expected} bytes, got {found}")]
InvalidU256Length {
field: &'static str,
expected: usize,
found: usize,
},
#[error("Invalid Bytes32 length for field `{field}`: expected {expected} bytes, got {found}")]
InvalidBytes32Length {
field: &'static str,
expected: usize,
found: usize,
},
}
pub(crate) fn decode_evm_address(
value: &v1::EvmAddress,
field: &'static str,
) -> Result<Address, ProtoConversionError> {
if value.value.len() != 20 {
return Err(ProtoConversionError::InvalidEvmAddressLength {
field,
expected: 20,
found: value.value.len(),
});
}
Ok(Address::from_slice(&value.value))
}
fn decode_u256(value: &v1::U256, field: &'static str) -> Result<AlloyU256, ProtoConversionError> {
if value.value.len() != 32 {
return Err(ProtoConversionError::InvalidU256Length {
field,
expected: 32,
found: value.value.len(),
});
}
Ok(AlloyU256::from_be_slice(&value.value))
}
impl From<Address> for v1::EvmAddress {
fn from(value: Address) -> Self {
Self {
value: value.as_slice().to_vec(),
}
}
}
impl From<Address> for v1::MultiChainAddress {
fn from(value: Address) -> Self {
Self {
chain_address: Some(v1::multi_chain_address::ChainAddress::Evm(value.into())),
}
}
}
impl TryFrom<v1::MultiChainAddress> for Address {
type Error = ProtoConversionError;
fn try_from(value: v1::MultiChainAddress) -> Result<Self, Self::Error> {
let chain_address = value
.chain_address
.ok_or(ProtoConversionError::MissingField { field: "address" })?;
match chain_address {
v1::multi_chain_address::ChainAddress::Evm(evm) => decode_evm_address(&evm, "address"),
}
}
}
impl From<AlloyU256> for v1::U256 {
fn from(value: AlloyU256) -> Self {
Self {
value: value.to_be_bytes::<32>().to_vec(),
}
}
}
impl TryFrom<v1::U256> for AlloyU256 {
type Error = ProtoConversionError;
fn try_from(value: v1::U256) -> Result<Self, Self::Error> {
decode_u256(&value, "value")
}
}
impl From<[u8; 32]> for v1::Bytes32 {
fn from(value: [u8; 32]) -> Self {
Self {
value: value.to_vec(),
}
}
}
impl TryFrom<v1::SpendSpec> for SpendSpec {
type Error = ProtoConversionError;
fn try_from(value: v1::SpendSpec) -> Result<Self, Self::Error> {
fn addr(
field: Option<v1::MultiChainAddress>,
name: &'static str,
) -> Result<[u8; 32], ProtoConversionError> {
let evm = Address::try_from(
field.ok_or(ProtoConversionError::MissingField { field: name })?,
)?;
let mut out = [0u8; 32];
out[12..].copy_from_slice(evm.as_slice());
Ok(out)
}
fn u256(
field: Option<v1::U256>,
name: &'static str,
) -> Result<AlloyU256, ProtoConversionError> {
field
.ok_or(ProtoConversionError::MissingField { field: name })?
.try_into()
}
fn bytes32(
field: Option<v1::Bytes32>,
name: &'static str,
) -> Result<[u8; 32], ProtoConversionError> {
let val = field.ok_or(ProtoConversionError::MissingField { field: name })?;
val.value.try_into().map_err(|value: Vec<u8>| {
ProtoConversionError::InvalidBytes32Length {
field: name,
expected: 32,
found: value.len(),
}
})
}
Ok(SpendSpec {
srcSpender: addr(value.src_spender, "src_spender")?.into(),
srcDomain: value.src_domain,
srcToken: addr(value.src_token, "src_token")?.into(),
srcDepositContract: addr(value.src_deposit_contract, "src_deposit_contract")?.into(),
dstDomain: value.dst_domain,
dstToken: addr(value.dst_token, "dst_token")?.into(),
dstDeliveryContract: addr(value.dst_delivery_contract, "dst_delivery_contract")?.into(),
dstRecipient: addr(value.dst_recipient, "dst_recipient")?.into(),
dstCaller: addr(value.dst_caller, "dst_caller")?.into(),
amount: u256(value.amount, "amount")?,
salt: bytes32(value.salt, "salt")?.into(),
})
}
}
impl TryFrom<v1::SpendIntent> for SpendIntent {
type Error = ProtoConversionError;
fn try_from(value: v1::SpendIntent) -> Result<Self, Self::Error> {
let spec = value
.spec
.ok_or(ProtoConversionError::MissingField { field: "spec" })?;
let max_fee = value
.max_fee
.ok_or(ProtoConversionError::MissingField { field: "max_fee" })?
.try_into()?;
Ok(SpendIntent {
maxFee: max_fee,
expiryHeight: AlloyU256::from(value.expiry_height),
spec: spec.try_into()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_proto_intent() -> v1::SpendIntent {
let addr = Address::from([0x01; 20]);
v1::SpendIntent {
spec: Some(v1::SpendSpec {
src_spender: Some(addr.into()),
src_domain: 1,
src_token: Some(Address::from([0x02; 20]).into()),
src_deposit_contract: Some(Address::from([0x03; 20]).into()),
dst_domain: 2,
dst_token: Some(Address::from([0x04; 20]).into()),
dst_delivery_contract: Some(Address::from([0x05; 20]).into()),
dst_recipient: Some(Address::from([0x06; 20]).into()),
dst_caller: Some(Address::from([0x07; 20]).into()),
amount: Some(AlloyU256::from(1000u64).into()),
salt: Some([0xAA; 32].into()),
}),
max_fee: Some(AlloyU256::from(10u64).into()),
expiry_height: 1000,
}
}
#[test]
fn decode_spend_intent_success() {
let intent = sample_proto_intent();
let result: Result<SpendIntent, _> = intent.try_into();
assert!(result.is_ok());
}
#[test]
fn decode_spend_intent_error_missing_spec() {
let mut intent = sample_proto_intent();
intent.spec = None;
let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
assert!(matches!(
err,
ProtoConversionError::MissingField { field: "spec" }
));
}
#[test]
fn decode_spend_intent_error_missing_address() {
let mut intent = sample_proto_intent();
intent.spec.as_mut().unwrap().src_spender = None;
let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
assert!(matches!(
err,
ProtoConversionError::MissingField {
field: "src_spender"
}
));
}
#[test]
fn decode_spend_intent_error_missing_u256() {
let mut intent = sample_proto_intent();
intent.spec.as_mut().unwrap().amount = None;
let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
assert!(matches!(
err,
ProtoConversionError::MissingField { field: "amount" }
));
}
#[test]
fn decode_spend_intent_error_missing_bytes32() {
let mut intent = sample_proto_intent();
intent.spec.as_mut().unwrap().salt = None;
let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
assert!(matches!(
err,
ProtoConversionError::MissingField { field: "salt" }
));
}
}