#![cfg(feature = "client")]
use std::time::{SystemTime, UNIX_EPOCH};
use eyre::{eyre, Result};
use crate::commands::config::config_pb::{Chain, GetConfigResponse, Market};
use crate::orders::{derive_order_id, GaslessLockParams};
use crate::wallet::{CurveType, Wallet};
use super::send_order::arborter_pb::GaslessAuthorization;
const ARCH_EVM: &str = "EVM";
const ARCH_SOLANA: &str = "Solana";
const EVM_OPEN_DEADLINE_SECS: u64 = 3_600;
const EVM_FILL_DEADLINE_SECS: u64 = 86_400;
const SOLANA_DEADLINE_SLOT_BUFFER: u64 = 100;
pub async fn build_gasless_authorization(
config: &GetConfigResponse,
market: &Market,
side: i32,
wallet: &Wallet,
quantity_raw: &str,
price_raw: Option<&str>,
) -> Result<GaslessAuthorization> {
let OrderResolution {
origin_chain,
destination_chain,
input_token_address,
output_token_address,
amount_in,
amount_out,
} = resolve_order(config, market, side, quantity_raw, price_raw)?;
let nonce = unix_millis()?;
let order_id_bytes = derive_order_id(
wallet_pubkey_bytes(wallet).as_slice(),
nonce,
origin_chain.chain_id as u64,
destination_chain.chain_id as u64,
input_token_address.as_bytes(),
output_token_address.as_bytes(),
amount_in,
amount_out,
);
let order_id_hex = format!("0x{}", hex::encode(order_id_bytes));
match origin_chain.architecture.as_str() {
ARCH_EVM => {
build_evm(
origin_chain,
destination_chain,
wallet,
&input_token_address,
&output_token_address,
amount_in,
amount_out,
nonce,
order_id_hex,
)
.await
}
ARCH_SOLANA => {
build_solana(
origin_chain,
destination_chain,
wallet,
&input_token_address,
&output_token_address,
amount_in,
amount_out,
nonce,
order_id_bytes,
order_id_hex,
)
.await
}
other => Err(eyre!(
"gasless auth not implemented for chain architecture {other:?}"
)),
}
}
struct OrderResolution<'a> {
origin_chain: &'a Chain,
destination_chain: &'a Chain,
input_token_address: String,
output_token_address: String,
amount_in: u128,
amount_out: u128,
}
fn resolve_order<'a>(
config: &'a GetConfigResponse,
market: &Market,
side: i32,
quantity_raw: &str,
price_raw: Option<&str>,
) -> Result<OrderResolution<'a>> {
let (origin_net, origin_sym, dest_net, dest_sym) = match side {
1 => (
&market.quote_chain_network,
&market.quote_chain_token_symbol,
&market.base_chain_network,
&market.base_chain_token_symbol,
),
2 => (
&market.base_chain_network,
&market.base_chain_token_symbol,
&market.quote_chain_network,
&market.quote_chain_token_symbol,
),
other => {
return Err(eyre!(
"unsupported side {other} — expected 1 (Bid) or 2 (Ask)"
))
}
};
let origin_chain = config
.get_chain(origin_net)
.ok_or_else(|| eyre!("origin chain {origin_net:?} not found in config"))?;
let destination_chain = config
.get_chain(dest_net)
.ok_or_else(|| eyre!("destination chain {dest_net:?} not found in config"))?;
let input_token = config
.get_token(origin_net, origin_sym)
.ok_or_else(|| eyre!("token {origin_sym} on {origin_net} not found"))?;
let output_token = config
.get_token(dest_net, dest_sym)
.ok_or_else(|| eyre!("token {dest_sym} on {dest_net} not found"))?;
let quantity: u128 = quantity_raw
.parse()
.map_err(|e| eyre!("quantity_raw {quantity_raw:?} is not a u128: {e}"))?;
let price: Option<u128> = price_raw
.map(|s| {
s.parse::<u128>()
.map_err(|e| eyre!("price_raw {s:?} is not a u128: {e}"))
})
.transpose()?;
let pair_decimals = market.pair_decimals as u32;
let price_scale = 10u128
.checked_pow(pair_decimals)
.ok_or_else(|| eyre!("pair_decimals {pair_decimals} overflows u128 scale"))?;
let (amount_in, amount_out) = match (side, price) {
(1, Some(p)) => (
quantity
.checked_mul(p)
.ok_or_else(|| eyre!("amount_in overflow"))?
/ price_scale,
quantity,
),
(2, Some(p)) => (
quantity,
quantity
.checked_mul(p)
.ok_or_else(|| eyre!("amount_out overflow"))?
/ price_scale,
),
(_, None) => {
(quantity, quantity)
}
_ => unreachable!(),
};
Ok(OrderResolution {
origin_chain,
destination_chain,
input_token_address: input_token.address.clone(),
output_token_address: output_token.address.clone(),
amount_in,
amount_out,
})
}
#[cfg(feature = "evm")]
#[allow(clippy::too_many_arguments)]
async fn build_evm(
origin_chain: &Chain,
destination_chain: &Chain,
wallet: &Wallet,
input_token_address: &str,
output_token_address: &str,
amount_in: u128,
amount_out: u128,
nonce: u64,
order_id_hex: String,
) -> Result<GaslessAuthorization> {
use alloy_primitives::Address;
let now = unix_secs()?;
let open_deadline = now + EVM_OPEN_DEADLINE_SECS;
let fill_deadline = now + EVM_FILL_DEADLINE_SECS;
let depositor = wallet.address();
let dest_chain_id = destination_chain.chain_id.to_string();
let params = GaslessLockParams {
depositor_address: &depositor,
token_contract: input_token_address,
token_contract_destination_chain: output_token_address,
destination_chain_id: &dest_chain_id,
amount_in,
amount_out,
order_id: &order_id_hex,
deadline: fill_deadline,
nonce,
open_deadline,
user_signature: &[],
};
let arborter: Address = origin_chain
.instance_signer_address
.parse()
.map_err(|e| eyre!("invalid instance_signer_address on origin chain: {e}"))?;
let origin_settler: Address = origin_chain
.trade_contract
.as_ref()
.ok_or_else(|| eyre!("origin chain has no trade_contract configured"))?
.address
.parse()
.map_err(|e| eyre!("invalid trade_contract.address on origin chain: {e}"))?;
let digest = crate::evm::gasless_lock_signing_hash(
¶ms,
arborter,
origin_settler,
origin_chain.chain_id as u64,
)?;
let sig = wallet.sign_message(digest.as_slice()).await?;
if sig.len() != 65 {
return Err(eyre!(
"EVM gasless signature must be 65 bytes (r||s||v); got {}",
sig.len()
));
}
Ok(GaslessAuthorization {
user_signature: sig,
deadline: fill_deadline,
order_id: order_id_hex,
nonce,
open_deadline,
})
}
#[cfg(not(feature = "evm"))]
#[allow(clippy::too_many_arguments)]
async fn build_evm(
_: &Chain,
_: &Chain,
_: &Wallet,
_: &str,
_: &str,
_: u128,
_: u128,
_: u64,
_: String,
) -> Result<GaslessAuthorization> {
Err(eyre!(
"EVM gasless authorization requires the `evm` feature of the aspens crate"
))
}
#[cfg(feature = "solana")]
#[allow(clippy::too_many_arguments)]
async fn build_solana(
origin_chain: &Chain,
destination_chain: &Chain,
wallet: &Wallet,
input_token_address: &str,
output_token_address: &str,
amount_in: u128,
amount_out: u128,
nonce: u64,
order_id_bytes: [u8; 32],
order_id_hex: String,
) -> Result<GaslessAuthorization> {
use crate::solana::{gasless_lock_signing_message, OpenOrderArgs};
use solana_sdk::pubkey::Pubkey;
let rpc = solana_client::nonblocking::rpc_client::RpcClient::new(origin_chain.rpc_url.clone());
let current_slot = rpc
.get_slot()
.await
.map_err(|e| eyre!("solana get_slot: {e}"))?;
let deadline = current_slot + SOLANA_DEADLINE_SLOT_BUFFER;
let instance_pda: Pubkey = origin_chain
.trade_contract
.as_ref()
.ok_or_else(|| eyre!("origin chain has no trade_contract configured"))?
.address
.parse()
.map_err(|e| eyre!("invalid trade_contract.address on origin chain: {e}"))?;
let user_pubkey: Pubkey = wallet.address().parse().map_err(|e| {
eyre!(
"wallet address {:?} not a valid Solana pubkey: {e}",
wallet.address()
)
})?;
let input_token: Pubkey = input_token_address
.parse()
.map_err(|e| eyre!("input token {input_token_address:?} not a Solana pubkey: {e}"))?;
let output_token_bytes = parse_cross_chain_token_into_32(output_token_address)?;
let amount_in_u64 = u64::try_from(amount_in)
.map_err(|_| eyre!("Solana amount_in {amount_in} exceeds u64::MAX"))?;
let amount_out_u64 = u64::try_from(amount_out)
.map_err(|_| eyre!("Solana amount_out {amount_out} exceeds u64::MAX"))?;
let order = OpenOrderArgs {
order_id: order_id_bytes,
origin_chain_id: origin_chain.chain_id as u64,
destination_chain_id: destination_chain.chain_id as u64,
input_token,
input_amount: amount_in_u64,
output_token: output_token_bytes,
output_amount: amount_out_u64,
};
let message = gasless_lock_signing_message(&instance_pda, &user_pubkey, deadline, &order)?;
let sig = wallet.sign_message(&message).await?;
if sig.len() != 64 {
return Err(eyre!(
"Solana gasless signature must be 64 bytes (Ed25519); got {}",
sig.len()
));
}
let _ = nonce;
Ok(GaslessAuthorization {
user_signature: sig,
deadline,
order_id: order_id_hex,
nonce: 0,
open_deadline: 0,
})
}
#[cfg(not(feature = "solana"))]
#[allow(clippy::too_many_arguments)]
async fn build_solana(
_: &Chain,
_: &Chain,
_: &Wallet,
_: &str,
_: &str,
_: u128,
_: u128,
_: u64,
_: [u8; 32],
_: String,
) -> Result<GaslessAuthorization> {
Err(eyre!(
"Solana gasless authorization requires the `solana` feature of the aspens crate"
))
}
fn wallet_pubkey_bytes(wallet: &Wallet) -> Vec<u8> {
match wallet.curve() {
CurveType::Secp256k1 => {
let s = wallet.address();
let trimmed = s.strip_prefix("0x").unwrap_or(&s);
hex::decode(trimmed).unwrap_or_default()
}
CurveType::Ed25519 => bs58::decode(wallet.address())
.into_vec()
.unwrap_or_default(),
}
}
fn parse_cross_chain_token_into_32(addr: &str) -> Result<[u8; 32]> {
if let Ok(bytes) = bs58::decode(addr).into_vec() {
if bytes.len() == 32 {
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
return Ok(out);
}
}
let trimmed = addr.strip_prefix("0x").unwrap_or(addr);
let bytes = hex::decode(trimmed)
.map_err(|e| eyre!("token {addr:?} is neither base58 nor 0x-hex: {e}"))?;
if bytes.len() > 32 {
return Err(eyre!(
"token {addr:?} is {} bytes — too large for 32-byte output_token slot",
bytes.len()
));
}
let mut out = [0u8; 32];
out[32 - bytes.len()..].copy_from_slice(&bytes);
Ok(out)
}
fn unix_secs() -> Result<u64> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| eyre!("system clock before epoch: {e}"))?
.as_secs())
}
fn unix_millis() -> Result<u64> {
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| eyre!("system clock before epoch: {e}"))?
.as_millis();
u64::try_from(ms).map_err(|_| eyre!("unix millis overflow"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cross_chain_token_evm_pads_to_32() {
let addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; let bytes = parse_cross_chain_token_into_32(addr).unwrap();
assert_eq!(&bytes[..12], &[0u8; 12]);
assert_eq!(&bytes[12..], &hex::decode(&addr[2..]).unwrap()[..]);
}
#[test]
fn cross_chain_token_solana_fills_32() {
let pk = "Ed25519SigVerify111111111111111111111111111";
let bytes = parse_cross_chain_token_into_32(pk).unwrap();
assert_eq!(bytes.len(), 32);
}
#[test]
fn cross_chain_token_rejects_oversize() {
let too_big = format!("0x{}", "aa".repeat(33));
assert!(parse_cross_chain_token_into_32(&too_big).is_err());
}
}