#![cfg(feature = "client")]
use std::time::{SystemTime, UNIX_EPOCH};
use eyre::{Result, eyre};
use crate::commands::config::config_pb::{Chain, GetConfigResponse, Market};
use crate::orders::derive_order_id;
use crate::wallet::{CurveType, Wallet};
use super::send_order::arborter_pb::OrderAuthorization;
pub fn build_gasless_authorization(
config: &GetConfigResponse,
market: &Market,
side: i32,
wallet: &Wallet,
quantity_raw: &str,
price_raw: Option<&str>,
) -> Result<OrderAuthorization> {
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));
Ok(OrderAuthorization {
order_id: order_id_hex,
amount_in: amount_in.to_string(),
})
}
#[cfg_attr(test, derive(Debug))]
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 normalize(amount: u128, from_decimals: u32, to_decimals: u32) -> Result<u128> {
use std::cmp::Ordering;
match from_decimals.cmp(&to_decimals) {
Ordering::Equal => Ok(amount),
Ordering::Greater => {
let scale = 10u128
.checked_pow(from_decimals - to_decimals)
.ok_or_else(|| {
eyre!(
"normalize scale 10^{} overflows u128",
from_decimals - to_decimals
)
})?;
Ok(amount / scale)
}
Ordering::Less => {
let scale = 10u128
.checked_pow(to_decimals - from_decimals)
.ok_or_else(|| {
eyre!(
"normalize scale 10^{} overflows u128",
to_decimals - from_decimals
)
})?;
amount.checked_mul(scale).ok_or_else(|| {
eyre!(
"normalize: {amount} * 10^{} overflows u128",
to_decimals - from_decimals
)
})
}
}
}
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 input_decimals = input_token.decimals;
let output_decimals = output_token.decimals;
let pair_decimals = market.pair_decimals as u32;
let quantity: u128 = quantity_raw
.parse()
.map_err(|e| eyre!("quantity_raw {quantity_raw:?} is not a u128: {e}"))?;
let price: u128 = match price_raw {
Some(s) => s
.parse::<u128>()
.map_err(|e| eyre!("price_raw {s:?} is not a u128: {e}"))?,
None => {
return Err(eyre!(
"cross-chain orders require a limit price — a market order \
can't pre-commit the `amount_in` the arborter reserves. Use \
buy-limit / sell-limit with a slippage-capped price (e.g. \
price ≥ best ask × (1 + slippage) for a buy)."
));
}
};
let (amount_in, amount_out) = match side {
1 => {
let qty_quote_pair2 = quantity
.checked_mul(price)
.ok_or_else(|| eyre!("amount_in overflow: {quantity} * {price}"))?;
(
normalize(qty_quote_pair2, pair_decimals * 2, input_decimals)?,
normalize(quantity, pair_decimals, output_decimals)?,
)
}
2 => {
let qty_quote_pair2 = quantity
.checked_mul(price)
.ok_or_else(|| eyre!("amount_out overflow: {quantity} * {price}"))?;
(
normalize(quantity, pair_decimals, input_decimals)?,
normalize(qty_quote_pair2, pair_decimals * 2, output_decimals)?,
)
}
_ => unreachable!("side validated above"),
};
Ok(OrderResolution {
origin_chain,
destination_chain,
input_token_address: input_token.address.clone(),
output_token_address: output_token.address.clone(),
amount_in,
amount_out,
})
}
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 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 normalize_identity_when_decimals_match() {
assert_eq!(normalize(123_456_789, 6, 6).unwrap(), 123_456_789);
assert_eq!(normalize(0, 18, 18).unwrap(), 0);
assert_eq!(normalize(u128::MAX, 6, 6).unwrap(), u128::MAX);
}
#[test]
fn normalize_downscales_with_truncation() {
assert_eq!(
normalize(1_000_000_000_000_000_000, 18, 6).unwrap(),
1_000_000
);
assert_eq!(normalize(999_999_999_999, 18, 6).unwrap(), 0);
assert_eq!(normalize(1_999_999_999_999, 18, 6).unwrap(), 1);
}
#[test]
fn normalize_upscales_within_u128() {
assert_eq!(
normalize(1_000_000, 6, 18).unwrap(),
1_000_000_000_000_000_000
);
}
#[test]
fn normalize_upscale_overflow_errors_cleanly() {
assert!(normalize(u128::MAX, 0, 40).is_err());
}
fn config_with_market(
base_dec: u32,
quote_dec: u32,
pair_dec: i32,
) -> (GetConfigResponse, Market) {
use crate::commands::config::config_pb::{Chain, Configuration, Market, TradeContract};
use std::collections::HashMap;
let mut base_tokens = HashMap::new();
base_tokens.insert(
"BASE".to_string(),
crate::commands::config::config_pb::Token {
name: "Base".into(),
symbol: "BASE".into(),
address: "0xbase".into(),
token_id: None,
decimals: base_dec,
},
);
let mut quote_tokens = HashMap::new();
quote_tokens.insert(
"QUOTE".to_string(),
crate::commands::config::config_pb::Token {
name: "Quote".into(),
symbol: "QUOTE".into(),
address: "0xquote".into(),
token_id: None,
decimals: quote_dec,
},
);
let base_chain = Chain {
architecture: "evm".into(),
canonical_name: "base-chain".into(),
network: "base-net".into(),
chain_id: 1,
instance_signer_address: "0x0000000000000000000000000000000000000001".into(),
explorer_url: None,
rpc_url: "http://localhost".into(),
factory_address: "0xfactory".into(),
trade_contract: Some(TradeContract {
contract_id: None,
address: "0xtradecontract".into(),
}),
tokens: base_tokens,
};
let quote_chain = Chain {
architecture: "evm".into(),
canonical_name: "quote-chain".into(),
network: "quote-net".into(),
chain_id: 2,
instance_signer_address: "0x0000000000000000000000000000000000000002".into(),
explorer_url: None,
rpc_url: "http://localhost".into(),
factory_address: "0xfactory".into(),
trade_contract: Some(TradeContract {
contract_id: None,
address: "0xtradecontract".into(),
}),
tokens: quote_tokens,
};
let market = Market {
name: "BASE/QUOTE".into(),
base_chain_network: "base-net".into(),
quote_chain_network: "quote-net".into(),
base_chain_token_symbol: "BASE".into(),
quote_chain_token_symbol: "QUOTE".into(),
base_chain_token_decimals: base_dec as i32,
quote_chain_token_decimals: quote_dec as i32,
pair_decimals: pair_dec,
market_id: "base-net::0xbase::quote-net::0xquote".into(),
};
let config = GetConfigResponse {
config: Some(Configuration {
chains: vec![base_chain, quote_chain],
markets: vec![market.clone()],
}),
};
(config, market)
}
#[test]
fn resolve_buy_limit_same_decimals_market() {
let (config, market) = config_with_market(6, 6, 6);
let r = resolve_order(&config, &market, 1, "100000", Some("1000000")).unwrap();
assert_eq!(r.amount_in, 100_000);
assert_eq!(r.amount_out, 100_000);
}
#[test]
fn resolve_buy_limit_high_pair_decimals_market() {
let (config, market) = config_with_market(18, 6, 18);
let q = "100000000000000000"; let p = "1000000000000000000"; let r = resolve_order(&config, &market, 1, q, Some(p)).unwrap();
assert_eq!(r.amount_in, 100_000);
assert_eq!(r.amount_out, 100_000_000_000_000_000);
}
#[test]
fn resolve_sell_limit_mirrors_buy() {
let (config, market) = config_with_market(6, 6, 6);
let r = resolve_order(&config, &market, 2, "100000", Some("1000000")).unwrap();
assert_eq!(r.amount_in, 100_000);
assert_eq!(r.amount_out, 100_000);
}
#[test]
fn resolve_market_order_is_rejected() {
let (config, market) = config_with_market(6, 6, 6);
let err = resolve_order(&config, &market, 1, "100000", None).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("require a limit price"),
"expected market-order rejection message; got: {msg}"
);
let err = resolve_order(&config, &market, 2, "100000", None).unwrap_err();
assert!(err.to_string().contains("require a limit price"));
}
#[test]
fn resolve_rejects_unknown_side() {
let (config, market) = config_with_market(6, 6, 6);
assert!(resolve_order(&config, &market, 7, "100000", Some("1000000")).is_err());
}
}