use std::sync::Arc;
use alloy_primitives::Address;
use alloy_signer_local::PrivateKeySigner;
use alloy_primitives::U256;
use crate::{
app_data::{
build_app_data_doc, build_app_data_doc_full,
types::{Metadata, OrderClass, OrderClassKind, PartnerFee, Quote, Utm},
},
config::{Env, NATIVE_CURRENCY_ADDRESS, SETTLEMENT_CONTRACT, SupportedChainId, VAULT_RELAYER},
erc20::build_erc20_approve_calldata,
error::CowError,
onchain::OnchainReader,
order_book::{
Order, OrderBookApi,
types::{
AppDataObject, Auction, CompetitionOrderStatus, GetOrdersRequest, GetTradesRequest,
OrderCancellations, OrderCreation, OrderQuoteRequest, OrderQuoteResponse, QuoteSide,
SolverCompetition, TotalSurplus, Trade,
},
},
order_signing::{
build_order_typed_data, invalidate_order_calldata, set_pre_signature_calldata, sign_order,
sign_order_cancellations, types::UnsignedOrder,
},
trading::{
costs::compute_quote_amounts_and_costs,
types::{
LimitTradeParameters, LimitTradeParametersFromQuote, OrderPostingResult,
PostTradeAdditionalParams, QuoteResults, SwapAdvancedSettings, TradeParameters,
TradingAppDataInfo, TradingTransactionParams,
},
},
types::{EcdsaSigningScheme, OrderKind, TokenBalance},
};
pub const DEFAULT_QUOTE_VALIDITY: u32 = 1_800;
pub const DEFAULT_SLIPPAGE_BPS: u32 = 50;
pub const ETH_FLOW_DEFAULT_SLIPPAGE_BPS: u32 = DEFAULT_SLIPPAGE_BPS;
pub const GAS_LIMIT_DEFAULT: u64 = 150_000;
#[must_use]
pub const fn calculate_gas_margin(gas: u64) -> u64 {
gas * 120 / 100
}
const DEFAULT_APP_DATA: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
fn default_app_data_info() -> TradingAppDataInfo {
TradingAppDataInfo {
full_app_data: "{}".to_owned(),
app_data_keccak256: DEFAULT_APP_DATA.to_owned(),
}
}
#[must_use]
pub fn get_is_eth_flow_order(sell_token: alloy_primitives::Address) -> bool {
sell_token == NATIVE_CURRENCY_ADDRESS
}
#[must_use]
pub fn get_default_utm_params() -> Utm {
Utm {
utm_source: Some("web".to_owned()),
utm_medium: Some(concat!("este-cowswap/", env!("CARGO_PKG_VERSION")).to_owned()),
utm_campaign: Some("CoW Swap".to_owned()),
utm_term: Some("trading".to_owned()),
utm_content: None,
}
}
#[must_use]
pub fn swap_params_to_limit_order_params(
params: &TradeParameters,
quote: &OrderQuoteResponse,
) -> LimitTradeParametersFromQuote {
let sell_amount: alloy_primitives::U256 =
quote.quote.sell_amount.parse().map_or(alloy_primitives::U256::ZERO, |v| v);
let buy_amount: alloy_primitives::U256 =
quote.quote.buy_amount.parse().map_or(alloy_primitives::U256::ZERO, |v| v);
LimitTradeParametersFromQuote {
sell_token: params.sell_token,
buy_token: params.buy_token,
sell_amount,
buy_amount,
quote_id: quote.id,
}
}
#[derive(Clone)]
pub struct TradingSdkConfig {
pub chain_id: SupportedChainId,
pub env: Env,
pub app_code: String,
pub slippage_bps: u32,
pub utm: Option<Utm>,
pub partner_fee: Option<PartnerFee>,
pub rpc_url: Option<String>,
pub orderbook_client: Option<Arc<dyn crate::traits::OrderbookClient>>,
}
impl std::fmt::Debug for TradingSdkConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TradingSdkConfig")
.field("chain_id", &self.chain_id)
.field("env", &self.env)
.field("app_code", &self.app_code)
.field("slippage_bps", &self.slippage_bps)
.field("utm", &self.utm)
.field("partner_fee", &self.partner_fee)
.field("rpc_url", &self.rpc_url)
.field("orderbook_client", &self.orderbook_client.as_ref().map(|_| ".."))
.finish()
}
}
impl TradingSdkConfig {
#[must_use]
pub fn prod(chain_id: SupportedChainId, app_code: impl Into<String>) -> Self {
Self {
chain_id,
env: Env::Prod,
app_code: app_code.into(),
slippage_bps: DEFAULT_SLIPPAGE_BPS,
utm: None,
partner_fee: None,
rpc_url: None,
orderbook_client: None,
}
}
#[must_use]
pub fn staging(chain_id: SupportedChainId, app_code: impl Into<String>) -> Self {
Self {
chain_id,
env: Env::Staging,
app_code: app_code.into(),
slippage_bps: DEFAULT_SLIPPAGE_BPS,
utm: None,
partner_fee: None,
rpc_url: None,
orderbook_client: None,
}
}
#[must_use]
pub const fn with_slippage_bps(mut self, bps: u32) -> Self {
self.slippage_bps = bps;
self
}
#[must_use]
pub fn with_utm(mut self, utm: Utm) -> Self {
self.utm = Some(utm);
self
}
#[must_use]
pub fn with_partner_fee(mut self, fee: PartnerFee) -> Self {
self.partner_fee = Some(fee);
self
}
#[must_use]
pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
self.rpc_url = Some(rpc_url.into());
self
}
#[must_use]
pub fn with_orderbook_client(
mut self,
client: Arc<dyn crate::traits::OrderbookClient>,
) -> Self {
self.orderbook_client = Some(client);
self
}
}
#[derive(Clone)]
pub struct TradingSdk {
config: Arc<TradingSdkConfig>,
api: Arc<OrderBookApi>,
signer: Arc<PrivateKeySigner>,
orderbook_client: Option<Arc<dyn crate::traits::OrderbookClient>>,
}
impl std::fmt::Debug for TradingSdk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TradingSdk")
.field("config", &self.config)
.field("api", &self.api)
.field("signer", &self.signer)
.field("orderbook_client", &self.orderbook_client.as_ref().map(|_| ".."))
.finish()
}
}
impl TradingSdk {
pub fn new(config: TradingSdkConfig, private_key_hex: &str) -> Result<Self, CowError> {
let key = private_key_hex.trim_start_matches("0x");
let signer: PrivateKeySigner = key
.parse()
.map_err(|e: alloy_signer_local::LocalSignerError| CowError::Signing(e.to_string()))?;
let api = OrderBookApi::new(config.chain_id, config.env);
let orderbook_client = config.orderbook_client.clone();
Ok(Self {
config: Arc::new(config),
api: Arc::new(api),
signer: Arc::new(signer),
orderbook_client,
})
}
pub fn new_with_url(
config: TradingSdkConfig,
private_key_hex: &str,
base_url: impl Into<String>,
) -> Result<Self, CowError> {
let key = private_key_hex.trim_start_matches("0x");
let signer: PrivateKeySigner = key
.parse()
.map_err(|e: alloy_signer_local::LocalSignerError| CowError::Signing(e.to_string()))?;
let api = OrderBookApi::new_with_url(config.chain_id, config.env, base_url);
let orderbook_client = config.orderbook_client.clone();
Ok(Self {
config: Arc::new(config),
api: Arc::new(api),
signer: Arc::new(signer),
orderbook_client,
})
}
#[must_use]
pub fn with_orderbook(mut self, client: Arc<dyn crate::traits::OrderbookClient>) -> Self {
self.orderbook_client = Some(client);
self
}
#[allow(dead_code, reason = "public API surface for future integration and downstream use")]
fn resolve_orderbook(&self) -> Arc<dyn crate::traits::OrderbookClient> {
if let Some(ref client) = self.orderbook_client {
Arc::clone(client)
} else {
Arc::clone(&self.api) as Arc<dyn crate::traits::OrderbookClient>
}
}
pub async fn get_quote(&self, params: TradeParameters) -> Result<QuoteResults, CowError> {
get_quote_impl(&self.config, &self.api, &self.signer, params, None).await
}
pub async fn post_swap_order_from_quote(
&self,
quote: &QuoteResults,
scheme: Option<EcdsaSigningScheme>,
) -> Result<OrderPostingResult, CowError> {
post_order_impl(&self.config, &self.api, &self.signer, quote, scheme).await
}
pub async fn post_swap_order(
&self,
params: TradeParameters,
) -> Result<OrderPostingResult, CowError> {
let quote = self.get_quote(params).await?;
self.post_swap_order_from_quote("e, None).await
}
pub async fn get_quote_with_settings(
&self,
params: TradeParameters,
settings: &SwapAdvancedSettings,
) -> Result<QuoteResults, CowError> {
get_quote_impl(&self.config, &self.api, &self.signer, params, Some(settings)).await
}
pub async fn post_swap_order_with_settings(
&self,
params: TradeParameters,
settings: &SwapAdvancedSettings,
) -> Result<OrderPostingResult, CowError> {
let quote =
get_quote_impl(&self.config, &self.api, &self.signer, params, Some(settings)).await?;
post_order_impl(&self.config, &self.api, &self.signer, "e, None).await
}
pub async fn get_order(&self, uid: &str) -> Result<Order, CowError> {
self.api.get_order(uid).await
}
pub async fn off_chain_cancel_orders(
&self,
order_uids: Vec<String>,
signing_scheme: EcdsaSigningScheme,
) -> Result<(), CowError> {
let uid_refs: Vec<&str> = order_uids.iter().map(String::as_str).collect();
let signing = sign_order_cancellations(
&uid_refs,
self.config.chain_id.as_u64(),
&self.signer,
signing_scheme,
)
.await?;
let body = OrderCancellations { order_uids, signature: signing.signature, signing_scheme };
self.api.cancel_orders(&body).await
}
pub async fn off_chain_cancel_order(
&self,
order_uid: String,
signing_scheme: EcdsaSigningScheme,
) -> Result<(), CowError> {
self.off_chain_cancel_orders(vec![order_uid], signing_scheme).await
}
pub async fn post_limit_order(
&self,
params: LimitTradeParameters,
scheme: Option<EcdsaSigningScheme>,
) -> Result<OrderPostingResult, CowError> {
post_limit_order_impl(&self.config, &self.api, &self.signer, params, scheme).await
}
#[must_use]
pub fn address(&self) -> Address {
self.signer.address()
}
#[must_use]
pub fn get_order_link(&self, order_uid: &str) -> String {
crate::config::chain::order_explorer_link(self.config.chain_id, order_uid)
}
pub async fn get_native_price(
&self,
token: alloy_primitives::Address,
) -> Result<f64, CowError> {
self.api.get_native_price(token).await
}
pub async fn get_auction(&self) -> Result<Auction, CowError> {
self.api.get_auction().await
}
pub async fn get_trades(
&self,
owner: alloy_primitives::Address,
limit: Option<u32>,
) -> Result<Vec<Trade>, CowError> {
self.api.get_trades_for_account(owner, limit).await
}
pub async fn get_orders_for_account(
&self,
owner: alloy_primitives::Address,
limit: Option<u32>,
) -> Result<Vec<Order>, CowError> {
self.api.get_orders_for_account(owner, limit).await
}
pub async fn get_limit_trade_parameters(
&self,
params: TradeParameters,
) -> Result<LimitTradeParameters, CowError> {
let quote = get_quote_impl(&self.config, &self.api, &self.signer, params, None).await?;
let costs = "e.amounts_and_costs;
let order = "e.order_to_sign;
Ok(LimitTradeParameters {
kind: order.kind,
sell_token: order.sell_token,
buy_token: order.buy_token,
sell_amount: costs.after_network_costs.sell_amount,
buy_amount: costs.after_network_costs.buy_amount,
receiver: Some(order.receiver),
valid_for: None,
valid_to: None,
partially_fillable: order.partially_fillable,
app_data: None,
partner_fee: None,
})
}
#[must_use]
pub const fn get_limit_trade_parameters_from_quote(
&self,
quote: &QuoteResults,
) -> LimitTradeParameters {
let costs = "e.amounts_and_costs;
let order = "e.order_to_sign;
LimitTradeParameters {
kind: order.kind,
sell_token: order.sell_token,
buy_token: order.buy_token,
sell_amount: costs.after_network_costs.sell_amount,
buy_amount: costs.after_network_costs.buy_amount,
receiver: Some(order.receiver),
valid_for: None,
valid_to: None,
partially_fillable: order.partially_fillable,
app_data: None,
partner_fee: None,
}
}
pub async fn post_presign_order(
&self,
order: &UnsignedOrder,
) -> Result<OrderPostingResult, CowError> {
use crate::order_signing::presign_result;
let owner = self.signer.address();
let signing = presign_result(owner);
let order_id = self
.api
.send_order(&OrderCreation {
sell_token: order.sell_token,
buy_token: order.buy_token,
receiver: order.receiver,
sell_amount: order.sell_amount.to_string(),
buy_amount: order.buy_amount.to_string(),
valid_to: order.valid_to,
app_data: format!("0x{}", alloy_primitives::hex::encode(order.app_data)),
fee_amount: order.fee_amount.to_string(),
kind: order.kind,
partially_fillable: order.partially_fillable,
sell_token_balance: order.sell_token_balance,
buy_token_balance: order.buy_token_balance,
signing_scheme: signing.signing_scheme,
signature: signing.signature.clone(),
from: owner,
quote_id: None,
})
.await?;
Ok(OrderPostingResult {
order_id,
signing_scheme: signing.signing_scheme,
signature: signing.signature,
order_to_sign: order.clone(),
})
}
pub async fn post_eip1271_order(
&self,
order: &UnsignedOrder,
signature_bytes: &[u8],
) -> Result<OrderPostingResult, CowError> {
use crate::order_signing::eip1271_result;
let signing = eip1271_result(signature_bytes);
let owner = self.signer.address();
let order_id = self
.api
.send_order(&OrderCreation {
sell_token: order.sell_token,
buy_token: order.buy_token,
receiver: order.receiver,
sell_amount: order.sell_amount.to_string(),
buy_amount: order.buy_amount.to_string(),
valid_to: order.valid_to,
app_data: format!("0x{}", alloy_primitives::hex::encode(order.app_data)),
fee_amount: order.fee_amount.to_string(),
kind: order.kind,
partially_fillable: order.partially_fillable,
sell_token_balance: order.sell_token_balance,
buy_token_balance: order.buy_token_balance,
signing_scheme: signing.signing_scheme,
signature: signing.signature.clone(),
from: owner,
quote_id: None,
})
.await?;
Ok(OrderPostingResult {
order_id,
signing_scheme: signing.signing_scheme,
signature: signing.signature,
order_to_sign: order.clone(),
})
}
pub fn get_pre_sign_transaction(
&self,
order_uid: &str,
signed: bool,
) -> Result<TradingTransactionParams, CowError> {
let data = set_pre_signature_calldata(order_uid, signed)?;
Ok(TradingTransactionParams {
data,
to: SETTLEMENT_CONTRACT,
gas_limit: GAS_LIMIT_DEFAULT,
value: U256::ZERO,
})
}
pub fn get_on_chain_cancellation(
&self,
order_uid: &str,
) -> Result<TradingTransactionParams, CowError> {
let data = invalidate_order_calldata(order_uid)?;
Ok(TradingTransactionParams {
data,
to: SETTLEMENT_CONTRACT,
gas_limit: GAS_LIMIT_DEFAULT,
value: U256::ZERO,
})
}
pub async fn get_order_status(
&self,
order_uid: &str,
) -> Result<CompetitionOrderStatus, CowError> {
self.api.get_order_status(order_uid).await
}
pub async fn get_orders_by_tx(&self, tx_hash: &str) -> Result<Vec<Order>, CowError> {
self.api.get_orders_by_tx(tx_hash).await
}
pub async fn get_order_multi_env(&self, uid: &str) -> Result<Order, CowError> {
self.api.get_order_multi_env(uid).await
}
pub async fn get_orders(&self, req: &GetOrdersRequest) -> Result<Vec<Order>, CowError> {
self.api.get_orders(req).await
}
pub async fn get_trades_with_request(
&self,
req: &GetTradesRequest,
) -> Result<Vec<Trade>, CowError> {
self.api.get_trades_with_request(req).await
}
pub async fn get_solver_competition(
&self,
auction_id: i64,
) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition(auction_id).await
}
pub async fn get_solver_competition_by_tx(
&self,
tx_hash: &str,
) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition_by_tx(tx_hash).await
}
pub async fn get_solver_competition_latest(&self) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition_latest().await
}
pub async fn get_solver_competition_v2(
&self,
auction_id: i64,
) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition_v2(auction_id).await
}
pub async fn get_solver_competition_by_tx_v2(
&self,
tx_hash: &str,
) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition_by_tx_v2(tx_hash).await
}
pub async fn get_solver_competition_latest_v2(&self) -> Result<SolverCompetition, CowError> {
self.api.get_solver_competition_latest_v2().await
}
pub async fn get_total_surplus(
&self,
address: alloy_primitives::Address,
) -> Result<TotalSurplus, CowError> {
self.api.get_total_surplus(address).await
}
pub async fn get_app_data(&self, app_data_hash: &str) -> Result<AppDataObject, CowError> {
self.api.get_app_data(app_data_hash).await
}
pub async fn upload_app_data(
&self,
app_data_hash: &str,
full_app_data: &str,
) -> Result<AppDataObject, CowError> {
self.api.upload_app_data(app_data_hash, full_app_data).await
}
pub async fn upload_app_data_auto(
&self,
full_app_data: &str,
) -> Result<AppDataObject, CowError> {
self.api.upload_app_data_auto(full_app_data).await
}
#[must_use]
pub fn get_vault_relayer_approve_transaction(
&self,
token: Address,
amount: U256,
) -> TradingTransactionParams {
TradingTransactionParams {
data: build_erc20_approve_calldata(VAULT_RELAYER, amount),
to: token,
gas_limit: GAS_LIMIT_DEFAULT,
value: U256::ZERO,
}
}
pub async fn get_cow_protocol_allowance(
&self,
owner: Address,
sell_token: Address,
) -> Result<U256, CowError> {
let rpc_url = self.config.rpc_url.as_deref().ok_or_else(|| CowError::Rpc {
code: -1,
message: "no RPC URL configured; use TradingSdkConfig::with_rpc_url".into(),
})?;
let reader = OnchainReader::new(rpc_url);
reader.erc20_allowance(sell_token, owner, VAULT_RELAYER).await
}
pub async fn get_version(&self) -> Result<String, CowError> {
self.api.get_version().await
}
pub async fn get_eth_flow_transaction(
&self,
order: &crate::ethflow::EthFlowOrderData,
) -> Result<crate::ethflow::EthFlowTransaction, CowError> {
use crate::{
config::{ETH_FLOW_PROD, ETH_FLOW_STAGING},
ethflow::build_eth_flow_transaction,
};
let contract = match self.config.env {
crate::config::Env::Prod => ETH_FLOW_PROD,
crate::config::Env::Staging => ETH_FLOW_STAGING,
};
Ok(build_eth_flow_transaction(contract, order))
}
}
#[must_use]
#[allow(clippy::too_many_arguments, reason = "domain parameters that belong together")]
pub fn get_order_to_sign(
chain_id: SupportedChainId,
from: Address,
is_eth_flow: bool,
_network_costs_amount: U256,
apply_costs_slippage_and_fees: bool,
params: &LimitTradeParameters,
app_data_keccak256: &str,
) -> UnsignedOrder {
let slippage_bps = params
.valid_for
.map(|_| get_default_slippage_bps(chain_id, is_eth_flow))
.unwrap_or_else(|| get_default_slippage_bps(chain_id, is_eth_flow));
let _ = slippage_bps;
let receiver = params.receiver.map_or(from, |r| r);
let valid_to = if let Some(v) = params.valid_to {
v
} else {
let valid_for = params.valid_for.map_or(DEFAULT_QUOTE_VALIDITY, |v| v);
get_order_deadline_from_now(valid_for)
};
let mut sell_amount = params.sell_amount;
let mut buy_amount = params.buy_amount;
if apply_costs_slippage_and_fees {
let default_slippage = get_default_slippage_bps(chain_id, is_eth_flow);
let is_sell = params.kind.is_sell();
if is_sell {
buy_amount =
buy_amount * U256::from(10_000u32 - default_slippage) / U256::from(10_000u32);
} else {
sell_amount =
sell_amount * U256::from(10_000u32 + default_slippage) / U256::from(10_000u32);
}
}
let app_data = parse_app_data_hex(app_data_keccak256);
UnsignedOrder {
sell_token: params.sell_token,
buy_token: params.buy_token,
sell_amount,
buy_amount,
valid_to,
kind: params.kind,
partially_fillable: params.partially_fillable,
app_data,
receiver,
fee_amount: U256::ZERO,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
}
}
#[must_use]
pub const fn get_order_typed_data(
chain_id: SupportedChainId,
order_to_sign: UnsignedOrder,
) -> crate::order_signing::types::OrderTypedData {
build_order_typed_data(order_to_sign, chain_id.as_u64())
}
#[must_use]
pub const fn get_default_slippage_bps(chain_id: SupportedChainId, is_eth_flow: bool) -> u32 {
let _ = chain_id; if is_eth_flow { ETH_FLOW_DEFAULT_SLIPPAGE_BPS } else { DEFAULT_SLIPPAGE_BPS }
}
pub fn get_slippage_percent(
is_sell: bool,
sell_amount_before_network_costs: U256,
sell_amount_after_network_costs: U256,
slippage: U256,
) -> Result<f64, CowError> {
let scale = U256::from(1_000_000u64);
let sell_amount =
if is_sell { sell_amount_after_network_costs } else { sell_amount_before_network_costs };
if sell_amount.is_zero() {
return Err(CowError::Signing(format!("sell_amount must be greater than 0: {sell_amount}")));
}
let result = if is_sell {
let numerator = scale * (sell_amount - slippage);
scale - numerator / sell_amount
} else {
let numerator = scale * (sell_amount + slippage);
numerator / sell_amount - scale
};
let result_u64: u64 = result.try_into().map_or(u64::MAX, |v| v);
Ok(result_u64 as f64 / 1_000_000.0)
}
#[must_use]
pub fn resolve_slippage_suggestion(
chain_id: SupportedChainId,
is_eth_flow: bool,
quote: &OrderQuoteResponse,
slippage_bps: u32,
) -> Option<u32> {
let costs = compute_quote_amounts_and_costs(quote, 0).ok()?;
let default_bps = get_default_slippage_bps(chain_id, is_eth_flow);
let suggested = crate::trading::slippage::suggest_slippage_bps(
&costs,
crate::trading::slippage::DEFAULT_FEE_SLIPPAGE_FACTOR_PCT,
crate::trading::slippage::DEFAULT_VOLUME_SLIPPAGE_BPS,
if is_eth_flow { default_bps } else { 0 },
);
(suggested > slippage_bps).then_some(suggested)
}
#[must_use]
pub fn adjust_eth_flow_order_params(
chain_id: SupportedChainId,
params: TradeParameters,
) -> TradeParameters {
let wrapped = crate::config::wrapped_native_currency(chain_id);
TradeParameters { sell_token: wrapped.address, ..params }
}
#[must_use]
pub fn adjust_eth_flow_limit_order_params(
chain_id: SupportedChainId,
params: LimitTradeParameters,
) -> LimitTradeParameters {
let wrapped = crate::config::wrapped_native_currency(chain_id);
LimitTradeParameters { sell_token: wrapped.address, ..params }
}
#[must_use]
pub fn get_trade_parameters_after_quote(
quote_parameters: TradeParameters,
original_sell_token: Address,
) -> TradeParameters {
TradeParameters { sell_token: original_sell_token, ..quote_parameters }
}
#[must_use]
pub const fn get_eth_flow_contract(chain_id: SupportedChainId, env: Env) -> Address {
crate::config::eth_flow_for_env(chain_id, env)
}
#[must_use]
pub const fn get_settlement_contract(chain_id: SupportedChainId, env: Env) -> Address {
crate::config::settlement_contract_for_env(chain_id, env)
}
pub fn get_eth_flow_cancellation(
chain_id: SupportedChainId,
env: Env,
order_uid: &str,
) -> Result<TradingTransactionParams, CowError> {
let contract = get_eth_flow_contract(chain_id, env);
let data = crate::order_signing::invalidate_order_calldata(order_uid)?;
Ok(TradingTransactionParams {
data,
to: contract,
gas_limit: GAS_LIMIT_DEFAULT,
value: U256::ZERO,
})
}
pub fn get_settlement_cancellation(
chain_id: SupportedChainId,
env: Env,
order_uid: &str,
) -> Result<TradingTransactionParams, CowError> {
let contract = get_settlement_contract(chain_id, env);
let data = crate::order_signing::invalidate_order_calldata(order_uid)?;
Ok(TradingTransactionParams {
data,
to: contract,
gas_limit: GAS_LIMIT_DEFAULT,
value: U256::ZERO,
})
}
#[must_use]
pub fn build_app_data(
app_code: &str,
slippage_bps: u32,
order_class: OrderClassKind,
partner_fee: Option<&PartnerFee>,
) -> TradingAppDataInfo {
let metadata = Metadata {
order_class: Some(OrderClass { order_class }),
quote: Some(Quote { slippage_bips: slippage_bps, smart_slippage: None }),
partner_fee: partner_fee.cloned(),
..Metadata::default()
};
build_app_data_doc_full(app_code, metadata)
.map(|(json, hash)| TradingAppDataInfo { full_app_data: json, app_data_keccak256: hash })
.unwrap_or_else(|_| default_app_data_info())
}
pub fn generate_app_data_from_doc(doc: &serde_json::Value) -> Result<TradingAppDataInfo, CowError> {
let json = serde_json::to_string(doc).map_err(|e| CowError::AppData(e.to_string()))?;
let value: serde_json::Value =
serde_json::from_str(&json).map_err(|e| CowError::AppData(e.to_string()))?;
let sorted = sort_keys_value(value);
let sorted_json =
serde_json::to_string(&sorted).map_err(|e| CowError::AppData(e.to_string()))?;
let hash = alloy_primitives::keccak256(sorted_json.as_bytes());
let hash_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
Ok(TradingAppDataInfo { full_app_data: sorted_json, app_data_keccak256: hash_hex })
}
fn sort_keys_value(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut pairs: Vec<(String, serde_json::Value)> =
map.into_iter().map(|(k, v)| (k, sort_keys_value(v))).collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
serde_json::Value::Object(pairs.into_iter().collect())
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(sort_keys_value).collect())
}
other @ (serde_json::Value::Null |
serde_json::Value::Bool(_) |
serde_json::Value::Number(_) |
serde_json::Value::String(_)) => other,
}
}
#[must_use]
pub fn calculate_unique_order_id(
chain_id: SupportedChainId,
order: &UnsignedOrder,
env: Env,
) -> String {
use crate::config::MAX_VALID_TO_EPOCH;
let wrapped = crate::config::wrapped_native_currency(chain_id);
let eth_flow_addr = crate::config::eth_flow_for_env(chain_id, env);
let adjusted = UnsignedOrder {
sell_token: wrapped.address,
valid_to: MAX_VALID_TO_EPOCH,
..order.clone()
};
crate::order_signing::compute_order_uid(chain_id.as_u64(), &adjusted, eth_flow_addr)
}
#[must_use]
pub const fn unsigned_order_for_signing(order: UnsignedOrder) -> UnsignedOrder {
order
}
#[must_use]
pub fn resolve_order_book_api(
chain_id: SupportedChainId,
env: Env,
existing: Option<OrderBookApi>,
) -> OrderBookApi {
if let Some(api) = existing {
let _ = (chain_id, env); return api;
}
OrderBookApi::new(chain_id, env)
}
#[allow(clippy::too_many_arguments, reason = "domain parameters that belong together")]
pub async fn post_cow_protocol_trade(
api: &OrderBookApi,
signer: &PrivateKeySigner,
app_data: &TradingAppDataInfo,
params: &LimitTradeParameters,
chain_id: SupportedChainId,
additional: &PostTradeAdditionalParams,
) -> Result<OrderPostingResult, CowError> {
let is_eth_flow = get_is_eth_flow_order(params.sell_token);
if is_eth_flow {
return Err(CowError::Signing(
"ETH-flow orders should use post_sell_native_currency_order".to_owned(),
));
}
let signing_scheme = additional
.signing_scheme
.as_ref()
.map(|s| match s {
crate::types::SigningScheme::EthSign => EcdsaSigningScheme::EthSign,
crate::types::SigningScheme::Eip712 |
crate::types::SigningScheme::Eip1271 |
crate::types::SigningScheme::PreSign => EcdsaSigningScheme::Eip712,
})
.map_or(EcdsaSigningScheme::Eip712, |v| v);
let owner = signer.address();
let from = params.receiver.map_or(owner, |r| r);
let _ = from;
let network_costs = additional
.network_costs_amount
.as_deref()
.and_then(|s| s.parse::<U256>().ok())
.map_or(U256::ZERO, |v| v);
let apply = additional.apply_costs_slippage_and_fees.map_or(true, core::convert::identity);
let order_to_sign = get_order_to_sign(
chain_id,
owner,
false,
network_costs,
apply,
params,
&app_data.app_data_keccak256,
);
#[allow(clippy::let_underscore_must_use, reason = "upload failure is non-fatal")]
let _ = api.upload_app_data(&app_data.app_data_keccak256, &app_data.full_app_data).await;
let signing = sign_order(&order_to_sign, chain_id.as_u64(), signer, signing_scheme).await?;
let order_id = api
.send_order(&OrderCreation {
sell_token: order_to_sign.sell_token,
buy_token: order_to_sign.buy_token,
receiver: order_to_sign.receiver,
sell_amount: order_to_sign.sell_amount.to_string(),
buy_amount: order_to_sign.buy_amount.to_string(),
valid_to: order_to_sign.valid_to,
app_data: app_data.full_app_data.clone(),
fee_amount: order_to_sign.fee_amount.to_string(),
kind: order_to_sign.kind,
partially_fillable: order_to_sign.partially_fillable,
sell_token_balance: order_to_sign.sell_token_balance,
buy_token_balance: order_to_sign.buy_token_balance,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature.clone(),
from: owner,
quote_id: None,
})
.await?;
Ok(OrderPostingResult {
order_id,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature,
order_to_sign,
})
}
pub async fn post_sell_native_currency_order(
api: &OrderBookApi,
app_data: &TradingAppDataInfo,
params: &LimitTradeParameters,
chain_id: SupportedChainId,
env: Env,
) -> Result<(OrderPostingResult, TradingTransactionParams), CowError> {
let eth_flow_addr = get_eth_flow_contract(chain_id, env);
let order_to_sign = get_order_to_sign(
chain_id,
eth_flow_addr, true,
U256::ZERO,
true,
params,
&app_data.app_data_keccak256,
);
let order_id = calculate_unique_order_id(chain_id, &order_to_sign, env);
let app_data_bytes = parse_app_data_hex(&app_data.app_data_keccak256);
let eth_flow_data = crate::ethflow::EthFlowOrderData {
buy_token: order_to_sign.buy_token,
receiver: order_to_sign.receiver,
sell_amount: order_to_sign.sell_amount,
buy_amount: order_to_sign.buy_amount,
app_data: app_data_bytes,
fee_amount: order_to_sign.fee_amount,
valid_to: order_to_sign.valid_to,
partially_fillable: order_to_sign.partially_fillable,
quote_id: 0, };
let calldata = crate::ethflow::encode_eth_flow_create_order(ð_flow_data);
let gas_limit = calculate_gas_margin(GAS_LIMIT_DEFAULT);
let tx = TradingTransactionParams {
data: calldata,
to: eth_flow_addr,
gas_limit,
value: order_to_sign.sell_amount,
};
#[allow(clippy::let_underscore_must_use, reason = "upload failure is non-fatal")]
let _ = api.upload_app_data(&app_data.app_data_keccak256, &app_data.full_app_data).await;
let result = OrderPostingResult {
order_id,
signing_scheme: crate::types::SigningScheme::Eip1271,
signature: String::new(),
order_to_sign,
};
Ok((result, tx))
}
async fn get_quote_impl(
config: &TradingSdkConfig,
api: &OrderBookApi,
signer: &PrivateKeySigner,
params: TradeParameters,
settings: Option<&SwapAdvancedSettings>,
) -> Result<QuoteResults, CowError> {
let owner = signer.address();
let slippage_bps = settings
.and_then(|s| s.slippage_bps)
.or(params.slippage_bps)
.map_or(config.slippage_bps, |v| v);
let effective_partner_fee = settings
.and_then(|s| s.partner_fee.clone())
.or_else(|| params.partner_fee.clone())
.or_else(|| config.partner_fee.clone());
let metadata = Metadata {
order_class: Some(OrderClass { order_class: OrderClassKind::Market }),
quote: Some(Quote { slippage_bips: slippage_bps, smart_slippage: None }),
utm: config.utm.clone(),
partner_fee: effective_partner_fee,
..Metadata::default()
};
let app_data_info = build_app_data_doc_full(&config.app_code, metadata)
.map(|(json, hash)| TradingAppDataInfo { full_app_data: json, app_data_keccak256: hash })
.unwrap_or_else(|_| default_app_data_info());
let app_data_hex = app_data_info.app_data_keccak256.clone();
let side = match params.kind {
OrderKind::Sell => QuoteSide::sell(params.amount.to_string()),
OrderKind::Buy => QuoteSide::buy(params.amount.to_string()),
};
let partially_fillable = params.partially_fillable.is_some_and(|v| v);
let req = OrderQuoteRequest {
sell_token: params.sell_token,
buy_token: params.buy_token,
receiver: params.receiver,
valid_to: params.valid_to,
app_data: app_data_hex.clone(),
partially_fillable,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
from: owner,
price_quality: crate::types::PriceQuality::Optimal,
signing_scheme: EcdsaSigningScheme::Eip712,
side,
};
let quote_response = api.get_quote(&req).await?;
let amounts_and_costs = compute_quote_amounts_and_costs("e_response, slippage_bps)?;
let receiver = params.receiver.map_or(owner, |r| r);
let valid_to = compute_order_valid_to(params.valid_to, params.valid_for);
let app_data = parse_app_data_hex(&app_data_hex);
let deadline = OrderDeadline { valid_to_unix: valid_to, partially_fillable };
let order_to_sign = build_unsigned_order(
app_data,
"e_response.quote,
receiver,
&amounts_and_costs,
deadline,
)?;
let order_typed_data = build_order_typed_data(order_to_sign.clone(), config.chain_id.as_u64());
Ok(QuoteResults {
order_to_sign,
order_typed_data,
quote_response,
amounts_and_costs,
suggested_slippage_bps: slippage_bps,
app_data_info,
})
}
#[must_use]
pub fn get_order_deadline_from_now(valid_for: u32) -> u32 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
(now + u64::from(valid_for)) as u32
}
fn compute_order_valid_to(valid_to_override: Option<u32>, valid_for: Option<u32>) -> u32 {
if let Some(v) = valid_to_override {
return v;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let ttl = u64::from(valid_for.map_or(DEFAULT_QUOTE_VALIDITY, |v| v));
(now + ttl) as u32
}
fn build_unsigned_order(
app_data: alloy_primitives::B256,
quote: &crate::order_book::types::QuoteData,
receiver: Address,
costs: &crate::trading::types::QuoteAmountsAndCosts,
deadline: OrderDeadline,
) -> Result<UnsignedOrder, CowError> {
let sell_amount = costs.after_slippage.sell_amount;
let buy_amount = costs.after_slippage.buy_amount;
let fee_amount = costs.network_fee.amount_in_sell_currency;
Ok(UnsignedOrder {
sell_token: quote.sell_token,
buy_token: quote.buy_token,
receiver,
sell_amount,
buy_amount,
valid_to: quote.valid_to.max(deadline.valid_to_unix),
app_data,
fee_amount,
kind: quote.kind,
partially_fillable: deadline.partially_fillable,
sell_token_balance: quote.sell_token_balance,
buy_token_balance: quote.buy_token_balance,
})
}
struct OrderDeadline {
valid_to_unix: u32,
partially_fillable: bool,
}
fn parse_app_data_hex(raw: &str) -> alloy_primitives::B256 {
let stripped = raw.trim_start_matches("0x");
let mut b = [0u8; 32];
if let Ok(decoded) = alloy_primitives::hex::decode(stripped) {
let len = decoded.len().min(32);
b[..len].copy_from_slice(&decoded[..len]);
}
alloy_primitives::B256::new(b)
}
async fn post_limit_order_impl(
config: &TradingSdkConfig,
api: &OrderBookApi,
signer: &alloy_signer_local::PrivateKeySigner,
params: LimitTradeParameters,
scheme: Option<EcdsaSigningScheme>,
) -> Result<OrderPostingResult, CowError> {
let owner = signer.address();
let signing_scheme = scheme.map_or(EcdsaSigningScheme::Eip712, |s| s);
let valid_to = if let Some(v) = params.valid_to {
v
} else {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let ttl = u64::from(params.valid_for.map_or(DEFAULT_QUOTE_VALIDITY, |v| v));
(now + ttl) as u32
};
let effective_fee = params.partner_fee.as_ref().or(config.partner_fee.as_ref()).cloned();
let app_data_hex = params.app_data.unwrap_or_else(|| {
let metadata = Metadata {
order_class: Some(OrderClass { order_class: OrderClassKind::Limit }),
utm: config.utm.clone(),
partner_fee: effective_fee,
..Metadata::default()
};
build_app_data_doc(&config.app_code, metadata)
.unwrap_or_else(|_| DEFAULT_APP_DATA.to_owned())
});
let app_data_bytes = parse_app_data_hex(&app_data_hex);
let order = UnsignedOrder {
sell_token: params.sell_token,
buy_token: params.buy_token,
receiver: params.receiver.map_or(owner, |r| r),
sell_amount: params.sell_amount,
buy_amount: params.buy_amount,
valid_to,
app_data: app_data_bytes,
fee_amount: alloy_primitives::U256::ZERO, kind: params.kind,
partially_fillable: params.partially_fillable,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
};
let signing = sign_order(&order, config.chain_id.as_u64(), signer, signing_scheme).await?;
let order_id = api
.send_order(&OrderCreation {
sell_token: order.sell_token,
buy_token: order.buy_token,
receiver: order.receiver,
sell_amount: order.sell_amount.to_string(),
buy_amount: order.buy_amount.to_string(),
valid_to: order.valid_to,
app_data: app_data_hex,
fee_amount: "0".to_owned(),
kind: order.kind,
partially_fillable: order.partially_fillable,
sell_token_balance: order.sell_token_balance,
buy_token_balance: order.buy_token_balance,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature.clone(),
from: owner,
quote_id: None,
})
.await?;
Ok(OrderPostingResult {
order_id,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature,
order_to_sign: order,
})
}
async fn post_order_impl(
config: &TradingSdkConfig,
api: &OrderBookApi,
signer: &PrivateKeySigner,
quote: &QuoteResults,
scheme: Option<EcdsaSigningScheme>,
) -> Result<OrderPostingResult, CowError> {
let signing_scheme = scheme.map_or(EcdsaSigningScheme::Eip712, |s| s);
let signing =
sign_order("e.order_to_sign, config.chain_id.as_u64(), signer, signing_scheme).await?;
let order_id = api
.send_order(&OrderCreation {
sell_token: quote.order_to_sign.sell_token,
buy_token: quote.order_to_sign.buy_token,
receiver: quote.order_to_sign.receiver,
sell_amount: quote.order_to_sign.sell_amount.to_string(),
buy_amount: quote.order_to_sign.buy_amount.to_string(),
valid_to: quote.order_to_sign.valid_to,
app_data: format!("0x{}", alloy_primitives::hex::encode(quote.order_to_sign.app_data)),
fee_amount: quote.order_to_sign.fee_amount.to_string(),
kind: quote.order_to_sign.kind,
partially_fillable: quote.order_to_sign.partially_fillable,
sell_token_balance: quote.order_to_sign.sell_token_balance,
buy_token_balance: quote.order_to_sign.buy_token_balance,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature.clone(),
from: signer.address(),
quote_id: quote.quote_response.id,
})
.await?;
Ok(OrderPostingResult {
order_id,
signing_scheme: signing_scheme.into_signing_scheme(),
signature: signing.signature,
order_to_sign: quote.order_to_sign.clone(),
})
}
pub async fn get_quote_raw(
api: &OrderBookApi,
req: &OrderQuoteRequest,
) -> Result<OrderQuoteResponse, CowError> {
api.get_quote(req).await
}
pub fn resolve_signer(private_key_hex: Option<&str>) -> Result<PrivateKeySigner, CowError> {
let key_hex =
private_key_hex.ok_or_else(|| CowError::Signing("no signer provided".to_owned()))?;
let key = key_hex.trim_start_matches("0x");
key.parse::<PrivateKeySigner>().map_err(|e| CowError::Signing(e.to_string()))
}
#[derive(Debug, Clone)]
pub struct QuoterParameters {
pub chain_id: SupportedChainId,
pub app_code: String,
pub account: Address,
}
#[must_use]
pub fn get_trader(
chain_id: SupportedChainId,
app_code: &str,
owner: Option<Address>,
signer: &PrivateKeySigner,
) -> QuoterParameters {
let account = owner.unwrap_or_else(|| signer.address());
QuoterParameters { chain_id, app_code: app_code.to_owned(), account }
}
#[derive(Debug, Clone)]
pub struct QuoteResultsWithSigner {
pub result: QuoteResults,
pub signer: PrivateKeySigner,
}
pub async fn get_quote_with_signer(
config: &TradingSdkConfig,
api: &OrderBookApi,
private_key_hex: &str,
params: TradeParameters,
settings: Option<&SwapAdvancedSettings>,
) -> Result<QuoteResultsWithSigner, CowError> {
let signer = resolve_signer(Some(private_key_hex))?;
let result = get_quote_impl(
&Arc::new(config.clone()),
&Arc::new(api.clone()),
&Arc::new(signer.clone()),
params,
settings,
)
.await?;
Ok(QuoteResultsWithSigner { result, signer })
}