use core::fmt;
use alloy_primitives::{Address, hex};
use crate::error::{Error, Result};
const VERSION_1: u16 = 0x0001;
const CHAIN_TYPE_EVM: u16 = 0x0000;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct InteropAddress {
pub version: u16,
pub chain_type: u16,
pub chain_ref: Vec<u8>,
pub address: Vec<u8>,
}
impl InteropAddress {
#[must_use]
pub fn evm(chain_id: u64, address: Address) -> Self {
Self {
version: VERSION_1,
chain_type: CHAIN_TYPE_EVM,
chain_ref: minimal_be_bytes(chain_id),
address: address.to_vec(),
}
}
#[must_use]
pub fn evm_no_chain(address: Address) -> Self {
Self {
version: VERSION_1,
chain_type: CHAIN_TYPE_EVM,
chain_ref: Vec::new(),
address: address.to_vec(),
}
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 6 {
return Err(Error::BufferTooShort { len: bytes.len() });
}
let version = u16::from_be_bytes([bytes[0], bytes[1]]);
if version != VERSION_1 {
return Err(Error::UnsupportedVersion { version });
}
let chain_type = u16::from_be_bytes([bytes[2], bytes[3]]);
let chain_ref_len = bytes[4] as usize;
let addr_len_offset = 5 + chain_ref_len;
if bytes.len() < addr_len_offset + 1 {
return Err(Error::TruncatedPayload {
expected: addr_len_offset + 1,
available: bytes.len(),
});
}
let addr_len = bytes[addr_len_offset] as usize;
let total = addr_len_offset + 1 + addr_len;
if bytes.len() < total {
return Err(Error::TruncatedPayload {
expected: total,
available: bytes.len(),
});
}
if chain_ref_len == 0 && addr_len == 0 {
return Err(Error::EmptyAddress);
}
let chain_ref = bytes[5..5 + chain_ref_len].to_vec();
let address = bytes[addr_len_offset + 1..total].to_vec();
Ok(Self {
version,
chain_type,
chain_ref,
address,
})
}
pub fn from_hex(s: &str) -> Result<Self> {
let bytes: Vec<u8> = hex::decode(s)?;
Self::decode(&bytes)
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let chain_ref_len = self.chain_ref.len();
let addr_len = self.address.len();
let mut buf = Vec::with_capacity(6 + chain_ref_len + addr_len);
buf.extend_from_slice(&self.version.to_be_bytes());
buf.extend_from_slice(&self.chain_type.to_be_bytes());
buf.push(u8::try_from(chain_ref_len).expect("chain_ref length exceeds u8::MAX"));
buf.extend_from_slice(&self.chain_ref);
buf.push(u8::try_from(addr_len).expect("address length exceeds u8::MAX"));
buf.extend_from_slice(&self.address);
buf
}
#[must_use]
pub fn to_hex(&self) -> String {
format!("0x{}", hex::encode(self.encode()))
}
#[must_use]
pub const fn is_evm(&self) -> bool {
self.chain_type == CHAIN_TYPE_EVM
}
#[must_use]
pub fn evm_chain_id(&self) -> Option<u64> {
if self.chain_ref.is_empty() || self.chain_ref.len() > 8 {
return None;
}
let mut padded = [0u8; 8];
let offset = 8 - self.chain_ref.len();
padded[offset..].copy_from_slice(&self.chain_ref);
Some(u64::from_be_bytes(padded))
}
#[must_use]
pub fn evm_address(&self) -> Option<Address> {
if self.address.len() == 20 {
Some(Address::from_slice(&self.address))
} else {
None
}
}
}
impl fmt::Debug for InteropAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InteropAddress")
.field("version", &format_args!("{:#06x}", self.version))
.field("chain_type", &format_args!("{:#06x}", self.chain_type))
.field("chain_ref", &hex::encode(&self.chain_ref))
.field("address", &hex::encode(&self.address))
.finish()
}
}
impl fmt::Display for InteropAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl core::str::FromStr for InteropAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::from_hex(s)
}
}
fn minimal_be_bytes(value: u64) -> Vec<u8> {
if value == 0 {
return vec![0];
}
let bytes = value.to_be_bytes();
let skip = bytes.iter().position(|&b| b != 0).unwrap_or(7);
bytes[skip..].to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_evm_mainnet() {
let addr: Address = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
.parse()
.expect("valid address");
let ia = InteropAddress::evm(1, addr);
assert_eq!(
ia.to_hex(),
"0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432"
);
}
#[test]
fn encode_evm_no_chain() {
let addr: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
.parse()
.expect("valid address");
let ia = InteropAddress::evm_no_chain(addr);
assert_eq!(
ia.to_hex(),
"0x000100000014d8da6bf26964af9d7eed9e03e53415d37aa96045"
);
}
#[test]
fn roundtrip() {
let addr: Address = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
.parse()
.expect("valid address");
let original = InteropAddress::evm(1, addr);
let bytes = original.encode();
let decoded = InteropAddress::decode(&bytes).expect("decode ok");
assert_eq!(original, decoded);
assert_eq!(original.to_hex(), decoded.to_hex());
}
#[test]
fn from_hex_with_prefix() {
let ia =
InteropAddress::from_hex("0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432")
.expect("decode ok");
assert_eq!(ia.version, 0x0001);
assert_eq!(ia.chain_type, 0x0000);
assert_eq!(ia.evm_chain_id(), Some(1));
assert_eq!(
ia.evm_address(),
Some(
"0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
.parse()
.expect("valid")
)
);
}
#[test]
fn from_str() {
let ia: InteropAddress = "0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432"
.parse()
.expect("parse ok");
assert!(ia.is_evm());
}
#[test]
fn display() {
let addr: Address = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
.parse()
.expect("valid address");
let ia = InteropAddress::evm(1, addr);
assert_eq!(format!("{ia}"), ia.to_hex());
}
#[test]
fn decode_too_short() {
assert!(InteropAddress::decode(&[0, 1, 0]).is_err());
}
#[test]
fn decode_unsupported_version() {
let err = InteropAddress::decode(&[0, 2, 0, 0, 0, 1, 0xFF]).unwrap_err();
assert!(err.to_string().contains("unsupported version"));
}
#[test]
fn decode_empty_address() {
let err = InteropAddress::decode(&[0, 1, 0, 0, 0, 0]).unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn ensip25_example_registry() {
let registry: Address = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
.parse()
.expect("valid");
let ia = InteropAddress::evm(1, registry);
assert_eq!(
ia.to_hex(),
"0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432"
);
}
#[test]
fn minimal_be_encoding() {
assert_eq!(minimal_be_bytes(0), vec![0]);
assert_eq!(minimal_be_bytes(1), vec![1]);
assert_eq!(minimal_be_bytes(255), vec![255]);
assert_eq!(minimal_be_bytes(256), vec![1, 0]);
assert_eq!(minimal_be_bytes(11_155_111), vec![0xAA, 0x36, 0xA7]);
}
}