use serde::Serialize;
use serde_json::{Value, json};
use tiny_keccak::{Hasher, Keccak};
use crate::error::ClientError;
use crate::rest::RestClient;
use crate::types::{
account::{
AgentSetAbstraction, ApproveAgent, ApproveBuilderFee, ConvertToMultiSigUser, PriorityBid,
SetDisplayName, SetReferrer, TopUpIsolatedOnlyMargin, UpdateIsolatedMargin, UpdateLeverage,
UserDexAbstraction, UserPortfolioMargin, UserSetAbstraction,
},
cross_chain::CrossChainSend,
encrypted::{EncryptedOrderSubmit, SubmitEncryptedOrder},
fba::FbaSubmit,
governance::{RegisterMetaliquidityOperator, SetMetaliquidityWhitelist},
meta_bridge::MbWithdraw,
order::{
BatchCancel, BatchModify, BatchOrder, CancelAllOrders, CancelByCloid, CancelOrder, Modify,
Order, OrderResponse, ScheduleCancel,
},
rfq::{RfqAccept, RfqRequest},
spot::{
EarnDeposit, EarnWithdraw, SpotCancel, SpotMarginClose, SpotMarginDeposit, SpotMarginOpen,
SpotMarginWithdraw, SpotOrder,
},
staking::{ClaimRewards, LinkStakingUser, TokenDelegate},
twap::{TwapCancel, TwapOrder},
vault::{CreateVault, VaultDistribute, VaultModify, VaultTransfer, VaultWithdraw},
};
use crate::wallet::{Eip712, Signature, TypedTradingAction, Wallet};
pub const MTF_MAINNET_CHAIN_ID: u64 = 8964;
pub const MTF_TESTNET_CHAIN_ID: u64 = 114514;
pub const MTF_CHAIN_ID: u64 = MTF_TESTNET_CHAIN_ID;
#[derive(Debug)]
pub struct Exchange<'a> {
pub(crate) client: &'a RestClient,
}
#[derive(Clone, Debug, Serialize)]
struct SignedEnvelope<'a> {
action: &'a Value,
nonce: u64,
signature: String,
}
impl<'a> Exchange<'a> {
pub async fn submit_order(
&self,
wallet: &Wallet,
order: &Order,
) -> Result<OrderResponse, ClientError> {
if order.owner != wallet.address() {
return Err(ClientError::Validation(format!(
"order.owner {} != wallet address {}",
order.owner,
wallet.address()
)));
}
let action = json!({ "type": "submit_order", "order": order });
self.post_typed_trade(wallet, action, TypedTradingAction::SubmitOrder(order))
.await
}
pub async fn cancel_order(
&self,
wallet: &Wallet,
cancel: &CancelOrder,
) -> Result<Value, ClientError> {
if cancel.owner != wallet.address() {
return Err(ClientError::Validation(format!(
"cancel.owner {} != wallet address {}",
cancel.owner,
wallet.address()
)));
}
let action = json!({ "type": "cancel_order", "cancel": cancel });
self.post_typed_trade(wallet, action, TypedTradingAction::CancelOrder(cancel))
.await
}
pub async fn set_position_mode(
&self,
wallet: &Wallet,
hedge: bool,
) -> Result<Value, ClientError> {
let action = json!({ "type": "set_position_mode", "params": { "hedge": hedge } });
self.post_signed(wallet, action).await
}
pub async fn spot_order(
&self,
wallet: &Wallet,
order: &SpotOrder,
) -> Result<OrderResponse, ClientError> {
let action = json!({ "type": "spot_order", "order": order });
self.post_typed_trade(wallet, action, TypedTradingAction::SpotOrder(order))
.await
}
pub async fn spot_cancel(
&self,
wallet: &Wallet,
cancel: &SpotCancel,
) -> Result<Value, ClientError> {
let action = json!({ "type": "spot_cancel", "cancel": cancel });
self.post_typed_trade(wallet, action, TypedTradingAction::SpotCancel(cancel))
.await
}
pub async fn spot_margin_deposit(
&self,
wallet: &Wallet,
params: &SpotMarginDeposit,
) -> Result<Value, ClientError> {
let action = json!({ "type": "spot_margin_deposit", "params": params });
self.post_signed(wallet, action).await
}
pub async fn spot_margin_withdraw(
&self,
wallet: &Wallet,
params: &SpotMarginWithdraw,
) -> Result<Value, ClientError> {
let action = json!({ "type": "spot_margin_withdraw", "params": params });
self.post_signed(wallet, action).await
}
pub async fn spot_margin_open(
&self,
wallet: &Wallet,
params: &SpotMarginOpen,
) -> Result<Value, ClientError> {
let action = json!({ "type": "spot_margin_open", "params": params });
self.post_signed(wallet, action).await
}
pub async fn spot_margin_close(
&self,
wallet: &Wallet,
params: &SpotMarginClose,
) -> Result<Value, ClientError> {
let action = json!({ "type": "spot_margin_close", "params": params });
self.post_signed(wallet, action).await
}
pub async fn earn_deposit(
&self,
wallet: &Wallet,
params: &EarnDeposit,
) -> Result<Value, ClientError> {
let action = json!({ "type": "earn_deposit", "params": params });
self.post_signed(wallet, action).await
}
pub async fn earn_withdraw(
&self,
wallet: &Wallet,
params: &EarnWithdraw,
) -> Result<Value, ClientError> {
let action = json!({ "type": "earn_withdraw", "params": params });
self.post_signed(wallet, action).await
}
pub async fn cancel_by_cloid(
&self,
wallet: &Wallet,
params: &CancelByCloid,
) -> Result<Value, ClientError> {
let action = json!({ "type": "cancel_by_cloid", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::CancelByCloid(params))
.await
}
pub async fn modify(&self, wallet: &Wallet, params: &Modify) -> Result<Value, ClientError> {
let action = json!({ "type": "modify", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::Modify(params))
.await
}
pub async fn batch_modify(
&self,
wallet: &Wallet,
params: &BatchModify,
) -> Result<Value, ClientError> {
let action = json!({ "type": "batch_modify", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::BatchModify(params))
.await
}
pub async fn batch_order(
&self,
wallet: &Wallet,
batch: &BatchOrder,
) -> Result<Value, ClientError> {
for (i, o) in batch.orders.iter().enumerate() {
if o.owner != wallet.address() {
return Err(ClientError::Validation(format!(
"batch order[{i}].owner {} != wallet address {}",
o.owner,
wallet.address()
)));
}
}
let action = json!({ "type": "batch_order", "params": batch });
self.post_typed_trade(wallet, action, TypedTradingAction::BatchOrder(batch))
.await
}
pub async fn batch_cancel(
&self,
wallet: &Wallet,
batch: &BatchCancel,
) -> Result<Value, ClientError> {
for (i, c) in batch.cancels.iter().enumerate() {
if c.owner != wallet.address() {
return Err(ClientError::Validation(format!(
"batch cancel[{i}].owner {} != wallet address {}",
c.owner,
wallet.address()
)));
}
}
let action = json!({ "type": "batch_cancel", "params": batch });
self.post_typed_trade(wallet, action, TypedTradingAction::BatchCancel(batch))
.await
}
pub async fn schedule_cancel(
&self,
wallet: &Wallet,
params: &ScheduleCancel,
) -> Result<Value, ClientError> {
let action = json!({ "type": "schedule_cancel", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::ScheduleCancel(params))
.await
}
pub async fn cancel_all_orders(
&self,
wallet: &Wallet,
params: &CancelAllOrders,
) -> Result<Value, ClientError> {
let action = json!({ "type": "cancel_all_orders", "params": params });
self.post_signed(wallet, action).await
}
pub async fn twap_order(
&self,
wallet: &Wallet,
params: &TwapOrder,
) -> Result<Value, ClientError> {
let action = json!({ "type": "twap_order", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::TwapOrder(params))
.await
}
pub async fn twap_cancel(
&self,
wallet: &Wallet,
params: &TwapCancel,
) -> Result<Value, ClientError> {
let action = json!({ "type": "twap_cancel", "params": params });
self.post_typed_trade(wallet, action, TypedTradingAction::TwapCancel(params))
.await
}
pub async fn update_leverage(
&self,
wallet: &Wallet,
params: &UpdateLeverage,
) -> Result<Value, ClientError> {
let action = json!({ "type": "update_leverage", "params": params });
self.post_signed(wallet, action).await
}
pub async fn update_isolated_margin(
&self,
wallet: &Wallet,
params: &UpdateIsolatedMargin,
) -> Result<Value, ClientError> {
let action = json!({ "type": "update_isolated_margin", "params": params });
self.post_signed(wallet, action).await
}
pub async fn top_up_isolated_only_margin(
&self,
wallet: &Wallet,
params: &TopUpIsolatedOnlyMargin,
) -> Result<Value, ClientError> {
let action = json!({ "type": "top_up_isolated_only_margin", "params": params });
self.post_signed(wallet, action).await
}
pub async fn user_portfolio_margin(
&self,
wallet: &Wallet,
params: &UserPortfolioMargin,
) -> Result<Value, ClientError> {
let action = json!({ "type": "user_portfolio_margin", "params": params });
self.post_signed(wallet, action).await
}
pub async fn pm_enroll(&self, wallet: &Wallet) -> Result<Value, ClientError> {
self.user_portfolio_margin(wallet, &UserPortfolioMargin { enroll: true })
.await
}
pub async fn pm_unenroll(&self, wallet: &Wallet) -> Result<Value, ClientError> {
self.user_portfolio_margin(wallet, &UserPortfolioMargin { enroll: false })
.await
}
pub async fn set_display_name(
&self,
wallet: &Wallet,
params: &SetDisplayName,
) -> Result<Value, ClientError> {
let action = json!({ "type": "set_display_name", "params": params });
self.post_signed(wallet, action).await
}
pub async fn set_referrer(
&self,
wallet: &Wallet,
params: &SetReferrer,
) -> Result<Value, ClientError> {
let action = json!({ "type": "set_referrer", "params": params });
self.post_signed(wallet, action).await
}
pub async fn approve_agent(
&self,
wallet: &Wallet,
params: &ApproveAgent,
) -> Result<Value, ClientError> {
let action = json!({ "type": "approve_agent", "params": params });
self.post_signed(wallet, action).await
}
pub async fn approve_builder_fee(
&self,
wallet: &Wallet,
params: &ApproveBuilderFee,
) -> Result<Value, ClientError> {
let action = json!({ "type": "approve_builder_fee", "params": params });
self.post_signed(wallet, action).await
}
pub async fn convert_to_multi_sig_user(
&self,
wallet: &Wallet,
params: &ConvertToMultiSigUser,
) -> Result<Value, ClientError> {
let action = json!({ "type": "convert_to_multi_sig_user", "params": params });
self.post_signed(wallet, action).await
}
pub async fn user_dex_abstraction(
&self,
wallet: &Wallet,
params: &UserDexAbstraction,
) -> Result<Value, ClientError> {
let action = json!({ "type": "user_dex_abstraction", "params": params });
self.post_signed(wallet, action).await
}
pub async fn user_set_abstraction(
&self,
wallet: &Wallet,
params: &UserSetAbstraction,
) -> Result<Value, ClientError> {
let action = json!({ "type": "user_set_abstraction", "params": params });
self.post_signed(wallet, action).await
}
pub async fn agent_set_abstraction(
&self,
wallet: &Wallet,
params: &AgentSetAbstraction,
) -> Result<Value, ClientError> {
let action = json!({ "type": "agent_set_abstraction", "params": params });
self.post_signed(wallet, action).await
}
pub async fn priority_bid(
&self,
wallet: &Wallet,
params: &PriorityBid,
) -> Result<Value, ClientError> {
let action = json!({ "type": "priority_bid", "params": params });
self.post_signed(wallet, action).await
}
pub async fn token_delegate(
&self,
wallet: &Wallet,
params: &TokenDelegate,
) -> Result<Value, ClientError> {
let action = json!({ "type": "token_delegate", "params": params });
self.post_signed(wallet, action).await
}
pub async fn claim_rewards(
&self,
wallet: &Wallet,
params: &ClaimRewards,
) -> Result<Value, ClientError> {
let action = json!({ "type": "claim_rewards", "params": params });
self.post_signed(wallet, action).await
}
pub async fn link_staking_user(
&self,
wallet: &Wallet,
params: &LinkStakingUser,
) -> Result<Value, ClientError> {
let action = json!({ "type": "link_staking_user", "params": params });
self.post_signed(wallet, action).await
}
pub async fn submit_encrypted_order(
&self,
wallet: &Wallet,
params: &SubmitEncryptedOrder,
) -> Result<Value, ClientError> {
let action = json!({ "type": "submit_encrypted_order", "params": params });
self.post_signed(wallet, action).await
}
pub async fn create_vault(
&self,
wallet: &Wallet,
params: &CreateVault,
) -> Result<Value, ClientError> {
let action = json!({ "type": "create_vault", "params": params });
self.post_signed(wallet, action).await
}
pub async fn vault_transfer(
&self,
wallet: &Wallet,
params: &VaultTransfer,
) -> Result<Value, ClientError> {
let action = json!({ "type": "vault_transfer", "params": params });
self.post_signed(wallet, action).await
}
pub async fn vault_modify(
&self,
wallet: &Wallet,
params: &VaultModify,
) -> Result<Value, ClientError> {
let action = json!({ "type": "vault_modify", "params": params });
self.post_signed(wallet, action).await
}
pub async fn vault_withdraw(
&self,
wallet: &Wallet,
params: &VaultWithdraw,
) -> Result<Value, ClientError> {
let action = json!({ "type": "vault_withdraw", "params": params });
self.post_signed(wallet, action).await
}
pub async fn vault_distribute(
&self,
wallet: &Wallet,
params: &VaultDistribute,
) -> Result<Value, ClientError> {
let action = json!({ "type": "vault_distribute", "params": params });
self.post_signed(wallet, action).await
}
pub async fn mb_withdraw(
&self,
wallet: &Wallet,
params: &MbWithdraw,
) -> Result<Value, ClientError> {
let action = json!({ "type": "mb_withdraw", "params": params });
self.post_signed(wallet, action).await
}
pub async fn set_metaliquidity_whitelist(
&self,
wallet: &Wallet,
params: &SetMetaliquidityWhitelist,
) -> Result<Value, ClientError> {
let action = json!({ "type": "set_metaliquidity_whitelist", "params": params });
self.post_signed(wallet, action).await
}
pub async fn register_metaliquidity_operator(
&self,
wallet: &Wallet,
params: &RegisterMetaliquidityOperator,
) -> Result<Value, ClientError> {
let action = json!({ "type": "register_metaliquidity_operator", "params": params });
self.post_signed(wallet, action).await
}
pub async fn rfq_request(
&self,
wallet: &Wallet,
params: &RfqRequest,
) -> Result<Value, ClientError> {
let action = json!({ "type": "rfq_request", "rfq": params });
self.post_signed(wallet, action).await
}
pub async fn rfq_accept(
&self,
wallet: &Wallet,
params: &RfqAccept,
) -> Result<Value, ClientError> {
let action = json!({ "type": "rfq_accept", "accept": params });
self.post_signed(wallet, action).await
}
pub async fn fba_submit(
&self,
wallet: &Wallet,
params: &FbaSubmit,
) -> Result<Value, ClientError> {
let action = json!({ "type": "fba_submit", "submit": params });
self.post_signed(wallet, action).await
}
pub async fn cross_chain_send(
&self,
wallet: &Wallet,
params: &CrossChainSend,
) -> Result<Value, ClientError> {
let action = json!({ "type": "cross_chain_send", "msg": params });
self.post_signed(wallet, action).await
}
pub async fn encrypted_order_submit(
&self,
wallet: &Wallet,
params: &EncryptedOrderSubmit,
) -> Result<Value, ClientError> {
let action = json!({ "type": "encrypted_order_submit", "encrypted": params });
self.post_signed(wallet, action).await
}
pub async fn post_signed<R: serde::de::DeserializeOwned>(
&self,
wallet: &Wallet,
action: Value,
) -> Result<R, ClientError> {
let (nonce, signature) = sign_action(wallet, &action)?;
let envelope = SignedEnvelope {
action: &action,
nonce,
signature,
};
self.client.post_json("/exchange", &envelope).await
}
}
pub(crate) fn sign_action(wallet: &Wallet, action: &Value) -> Result<(u64, String), ClientError> {
let nonce = next_nonce();
let digest = ActionSignedDigest { action, nonce };
let sig = wallet.sign_eip712(&digest)?;
Ok((nonce, sig.to_hex()))
}
struct ActionSignedDigest<'a> {
action: &'a Value,
nonce: u64,
}
impl Eip712 for ActionSignedDigest<'_> {
fn domain_separator(&self) -> [u8; 32] {
let name = "MetaFlux";
let version = "1";
let chain_id: u64 = MTF_CHAIN_ID;
let type_hash = keccak(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
.as_bytes(),
);
let name_hash = keccak(name.as_bytes());
let version_hash = keccak(version.as_bytes());
let mut buf = Vec::with_capacity(32 * 5);
buf.extend_from_slice(&type_hash);
buf.extend_from_slice(&name_hash);
buf.extend_from_slice(&version_hash);
let mut chain_be = [0u8; 32];
chain_be[24..].copy_from_slice(&chain_id.to_be_bytes());
buf.extend_from_slice(&chain_be);
let verifying_contract = [0u8; 20];
let mut verifying_padded = [0u8; 32];
verifying_padded[12..].copy_from_slice(&verifying_contract);
buf.extend_from_slice(&verifying_padded);
keccak(&buf)
}
fn struct_hash(&self) -> [u8; 32] {
let type_hash = keccak("MetaFluxAction(string action,uint64 nonce)".as_bytes());
let action_json = serde_json::to_string(self.action).unwrap_or_default();
let action_hash = keccak(action_json.as_bytes());
let mut nonce_be = [0u8; 32];
nonce_be[24..].copy_from_slice(&self.nonce.to_be_bytes());
let mut buf = Vec::with_capacity(32 * 3);
buf.extend_from_slice(&type_hash);
buf.extend_from_slice(&action_hash);
buf.extend_from_slice(&nonce_be);
keccak(&buf)
}
}
fn keccak(input: &[u8]) -> [u8; 32] {
let mut h = Keccak::v256();
h.update(input);
let mut out = [0u8; 32];
h.finalize(&mut out);
out
}
fn current_unix_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
pub(crate) fn next_nonce() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static NONCE_CLOCK: AtomicU64 = AtomicU64::new(0);
let now = current_unix_ms();
let mut prev = NONCE_CLOCK.load(Ordering::Relaxed);
loop {
let next = now.max(prev.saturating_add(1));
match NONCE_CLOCK.compare_exchange_weak(prev, next, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return next,
Err(observed) => prev = observed,
}
}
}
#[doc(hidden)]
pub fn _action_digest_for_test(action: &Value, nonce: u64) -> [u8; 32] {
let d = ActionSignedDigest { action, nonce };
d.to_digest()
}
#[doc(hidden)]
pub fn _recover_for_test(
digest: &[u8; 32],
sig: &Signature,
) -> Result<crate::wallet::Address, ClientError> {
crate::wallet::sign_recover_for_test_only(digest, sig)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_separator_is_deterministic() {
let a = ActionSignedDigest {
action: &json!({"type": "submit_order"}),
nonce: 1,
};
let b = ActionSignedDigest {
action: &json!({"type": "submit_order"}),
nonce: 1,
};
assert_eq!(a.domain_separator(), b.domain_separator());
}
#[test]
fn struct_hash_changes_with_nonce() {
let a = ActionSignedDigest {
action: &json!({"type": "submit_order"}),
nonce: 1,
};
let b = ActionSignedDigest {
action: &json!({"type": "submit_order"}),
nonce: 2,
};
assert_ne!(a.struct_hash(), b.struct_hash());
}
#[test]
fn struct_hash_changes_with_action() {
let a = ActionSignedDigest {
action: &json!({"type": "submit_order"}),
nonce: 1,
};
let b = ActionSignedDigest {
action: &json!({"type": "cancel_order"}),
nonce: 1,
};
assert_ne!(a.struct_hash(), b.struct_hash());
}
#[test]
fn native_action_kat_matches_server() {
let action_json = br#"{"type":"submit_order","order":{"owner":"0x000000000000000000000000000000000000beef","market":1,"side":"bid","kind":"limit","size":1000,"limit_px":5000000000000,"tif":"gtc","stp_mode":"cancel_oldest","reduce_only":false}}"#;
let nonce: u64 = 1_700_000_000_000;
let domain = ActionSignedDigest {
action: &json!({}),
nonce: 0,
}
.domain_separator();
let type_hash = keccak("MetaFluxAction(string action,uint64 nonce)".as_bytes());
let action_hash = keccak(action_json);
let mut nonce_be = [0u8; 32];
nonce_be[24..].copy_from_slice(&nonce.to_be_bytes());
let mut sh_buf = Vec::with_capacity(32 * 3);
sh_buf.extend_from_slice(&type_hash);
sh_buf.extend_from_slice(&action_hash);
sh_buf.extend_from_slice(&nonce_be);
let struct_hash = keccak(&sh_buf);
let mut d_buf = Vec::with_capacity(2 + 64);
d_buf.extend_from_slice(&[0x19, 0x01]);
d_buf.extend_from_slice(&domain);
d_buf.extend_from_slice(&struct_hash);
let digest = keccak(&d_buf);
let expected: [u8; 32] = [
0xf7, 0xaa, 0x10, 0x87, 0xf7, 0x9b, 0x30, 0xfb, 0x3f, 0x13, 0xa1, 0x90, 0x63, 0x6d,
0x32, 0xb3, 0x27, 0x20, 0xd5, 0x98, 0x41, 0x91, 0x99, 0x2d, 0x70, 0x7e, 0x2a, 0xfb,
0xca, 0x71, 0x6e, 0x0d,
];
assert_eq!(
digest, expected,
"SDK digest must equal server KAT f7aa10..6e0d; got {digest:02x?}"
);
}
}