use eyre::{eyre, Result};
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
}
#[derive(Debug, Clone)]
pub struct GaslessLockParams<'a> {
pub depositor_address: &'a str,
pub token_contract: &'a str,
pub token_contract_destination_chain: &'a str,
pub destination_chain_id: &'a str,
pub amount_in: u128,
pub amount_out: u128,
pub order_id: &'a str,
pub deadline: u64,
pub nonce: u64,
pub open_deadline: u64,
pub user_signature: &'a [u8],
}
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() {
if 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
))
}
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());
}
#[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");
}
}