use eyre::{Result, eyre};
use sha2::{Digest, Sha256};
#[allow(clippy::too_many_arguments)]
pub fn derive_order_id(
user_pubkey: &[u8],
client_nonce: u64,
origin_chain_id: u64,
destination_chain_id: u64,
input_token: &[u8],
output_token: &[u8],
input_amount: u128,
output_amount: u128,
) -> [u8; 32] {
let mut h = Sha256::new();
h.update(user_pubkey);
h.update(client_nonce.to_le_bytes());
h.update(origin_chain_id.to_le_bytes());
h.update(destination_chain_id.to_le_bytes());
h.update(input_token);
h.update(output_token);
h.update(input_amount.to_le_bytes());
h.update(output_amount.to_le_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
}
pub fn parse_destination_token_bytes32(token: &str) -> Result<[u8; 32]> {
let trimmed = token.trim();
if trimmed.is_empty() {
return Err(eyre!("empty destination token"));
}
if let Some(hex_body) = trimmed.strip_prefix("0x") {
return decode_hex_to_bytes32(hex_body, trimmed);
}
#[cfg(feature = "solana")]
if let Ok(raw) = bs58::decode(trimmed).into_vec()
&& raw.len() == 32
{
let mut out = [0u8; 32];
out.copy_from_slice(&raw);
return Ok(out);
}
if !trimmed.is_empty() && trimmed.len() <= 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit())
{
return decode_hex_to_bytes32(trimmed, trimmed);
}
#[cfg(feature = "solana")]
{
Err(eyre!(
"destination token '{}' is neither a 32-byte base58 pubkey nor a valid \
hex string of ≤32 bytes",
trimmed
))
}
#[cfg(not(feature = "solana"))]
Err(eyre!(
"non-hex destination token '{}' requires the `solana` feature",
trimmed
))
}
pub fn validate_settle_address(architecture: &str, address: &str) -> Result<()> {
if architecture.eq_ignore_ascii_case("solana") {
#[cfg(feature = "solana")]
return match bs58::decode(address).into_vec() {
Ok(raw) if raw.len() == 32 => Ok(()),
Ok(raw) => Err(eyre!(
"'{address}' base58-decodes to {} bytes, expected exactly 32",
raw.len()
)),
Err(e) => Err(eyre!("'{address}' is not a valid base58 pubkey: {e}")),
};
#[cfg(not(feature = "solana"))]
return Err(eyre!(
"cannot validate Solana address '{address}': the `solana` feature is not compiled in"
));
}
let Some(body) = address.strip_prefix("0x") else {
return Err(eyre!(
"EVM address '{address}' must carry the 0x prefix (the venue's ledger only \
canonicalizes 0x-prefixed addresses, so an unprefixed spelling would key a \
separate balance)"
));
};
if body.len() != 40 || !body.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(eyre!(
"EVM address '{address}' must be exactly 20 bytes of hex after the 0x prefix"
));
}
let has_lower = body.bytes().any(|b| b.is_ascii_lowercase());
let has_upper = body.bytes().any(|b| b.is_ascii_uppercase());
if has_lower && has_upper {
alloy_primitives::Address::parse_checksummed(address, None).map_err(|_| {
eyre!(
"EVM address '{address}' is mixed-case but fails its EIP-55 checksum — \
likely a typo; paste the address exactly, or all-lowercase to skip the check"
)
})?;
}
Ok(())
}
fn decode_hex_to_bytes32(hex_body: &str, display: &str) -> Result<[u8; 32]> {
if hex_body.is_empty() {
return Err(eyre!("empty hex body in '{}'", display));
}
if hex_body.len() > 64 {
return Err(eyre!(
"hex token '{}' has {} hex chars; max 64 (32 bytes)",
display,
hex_body.len()
));
}
if !hex_body.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(eyre!("hex token '{}' contains non-hex characters", display));
}
let normalized = if hex_body.len().is_multiple_of(2) {
hex_body.to_string()
} else {
format!("0{hex_body}")
};
let raw =
hex::decode(&normalized).map_err(|e| eyre!("invalid hex token '{}': {}", display, e))?;
let mut out = [0u8; 32];
out[32 - raw.len()..].copy_from_slice(&raw);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn order_id_is_deterministic() {
let a = derive_order_id(&[1; 32], 42, 1, 501, b"0xaaa", b"MintXYZ", 100, 200);
let b = derive_order_id(&[1; 32], 42, 1, 501, b"0xaaa", b"MintXYZ", 100, 200);
assert_eq!(a, b);
}
#[test]
fn order_id_changes_with_nonce() {
let a = derive_order_id(&[1; 32], 1, 1, 501, b"t1", b"t2", 100, 200);
let b = derive_order_id(&[1; 32], 2, 1, 501, b"t1", b"t2", 100, 200);
assert_ne!(a, b);
}
#[test]
fn order_id_endianness_is_le() {
let id = derive_order_id(&[], 0, 0, 0, &[], &[], 0, 0);
let mut h = Sha256::new();
h.update([0u8; 56]);
let mut want = [0u8; 32];
want.copy_from_slice(&h.finalize());
assert_eq!(id, want);
}
#[test]
fn parse_hex_20_byte_address_left_pads() {
let evm = "0x".to_string() + &"ab".repeat(20);
let bytes = parse_destination_token_bytes32(&evm).unwrap();
assert_eq!(&bytes[..12], &[0u8; 12]);
assert_eq!(&bytes[12..], &[0xabu8; 20]);
}
#[test]
fn parse_hex_32_byte_passes_through() {
let h = "0x".to_string() + &"cd".repeat(32);
let bytes = parse_destination_token_bytes32(&h).unwrap();
assert_eq!(bytes, [0xcdu8; 32]);
}
#[test]
fn parse_hex_without_0x_prefix_works() {
let h = "ab".repeat(20);
let bytes = parse_destination_token_bytes32(&h).unwrap();
assert_eq!(&bytes[12..], &[0xabu8; 20]);
}
#[cfg(feature = "solana")]
#[test]
fn parse_base58_solana_pubkey() {
let raw = [0x42u8; 32];
let b58 = bs58::encode(raw).into_string();
let bytes = parse_destination_token_bytes32(&b58).unwrap();
assert_eq!(bytes, raw);
}
#[test]
fn parse_rejects_too_long_hex() {
let h = "0x".to_string() + &"ab".repeat(33);
assert!(parse_destination_token_bytes32(&h).is_err());
}
#[test]
fn parse_rejects_empty() {
assert!(parse_destination_token_bytes32("").is_err());
assert!(parse_destination_token_bytes32(" ").is_err());
}
fn checksummed() -> String {
let addr = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"
.parse::<alloy_primitives::Address>()
.unwrap()
.to_checksum(None);
assert_eq!(addr, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
addr
}
#[test]
fn settle_address_accepts_a_checksummed_evm_address() {
assert!(validate_settle_address("evm", &checksummed()).is_ok());
}
#[test]
fn settle_address_accepts_a_lowercase_evm_address() {
assert!(validate_settle_address("evm", &checksummed().to_ascii_lowercase()).is_ok());
}
#[test]
fn settle_address_rejects_a_bad_evm_checksum() {
let good = checksummed();
let bad = good.replacen("aA", "Aa", 1);
assert_ne!(good, bad, "the swap must change something");
assert_ne!(
bad,
bad.to_ascii_lowercase(),
"the broken fixture must stay mixed-case, or it lands on the \
no-checksum-claimed path and is accepted for the wrong reason"
);
let err = validate_settle_address("evm", &bad).unwrap_err();
assert!(err.to_string().contains("checksum"), "{err}");
}
#[test]
fn settle_address_rejects_unprefixed_hex_on_evm() {
assert!(validate_settle_address("evm", &checksummed()[2..]).is_err());
}
#[test]
fn settle_address_rejects_wrong_length_or_garbage_on_evm() {
assert!(validate_settle_address("evm", "0xAbCdEf01").is_err());
assert!(
validate_settle_address("evm", "0xnot-hex-at-all-000000000000000000000000").is_err()
);
assert!(validate_settle_address("evm", "").is_err());
}
#[cfg(feature = "solana")]
#[test]
fn settle_address_accepts_a_32_byte_base58_pubkey() {
let pk = bs58::encode([7u8; 32]).into_string();
assert!(validate_settle_address("solana", &pk).is_ok());
assert!(validate_settle_address("Solana", &pk).is_ok());
}
#[cfg(feature = "solana")]
#[test]
fn settle_address_rejects_non_pubkeys_on_solana() {
assert!(validate_settle_address("solana", "abc").is_err());
assert!(validate_settle_address("solana", &checksummed()).is_err());
assert!(validate_settle_address("solana", "").is_err());
}
#[cfg(feature = "solana")]
#[test]
fn parse_ambiguous_base58_zero_pubkey_decodes_as_base58() {
let zero_pubkey_base58 = bs58::encode([0u8; 32]).into_string();
assert_eq!(zero_pubkey_base58, "11111111111111111111111111111111");
let parsed = parse_destination_token_bytes32(&zero_pubkey_base58).unwrap();
assert_eq!(parsed, [0u8; 32], "unprefixed 32-byte base58 wins");
let with_prefix = format!("0x{}", zero_pubkey_base58);
let parsed = parse_destination_token_bytes32(&with_prefix).unwrap();
let mut expected = [0u8; 32];
expected[16..].copy_from_slice(&[0x11u8; 16]);
assert_eq!(parsed, expected, "0x prefix forces hex");
}
}