use serde::Serialize;
use serde_json::{Value, json};
use crate::error::ClientError;
use crate::rest::exchange::{Exchange, MTF_CHAIN_ID, next_nonce};
use crate::wallet::{
Eip712, TypedAction, TypedActionDigest, TypedTradingAction, TypedTradingDigest, Wallet,
metaflux_chain_tag,
};
#[derive(Clone, Debug, Serialize)]
struct TypedSignedEnvelope<'a> {
action: &'a Value,
nonce: u64,
signature: String,
sig_scheme: &'static str,
}
fn mb_chain_to_u8(chain: crate::types::meta_bridge::MbChain) -> u8 {
match chain {
crate::types::meta_bridge::MbChain::Solana => 0,
crate::types::meta_bridge::MbChain::Base => 1,
crate::types::meta_bridge::MbChain::Arbitrum => 2,
}
}
fn mb_chain_name(chain: crate::types::meta_bridge::MbChain) -> &'static str {
match chain {
crate::types::meta_bridge::MbChain::Solana => "Solana",
crate::types::meta_bridge::MbChain::Base => "Base",
crate::types::meta_bridge::MbChain::Arbitrum => "Arbitrum",
}
}
impl<'a> Exchange<'a> {
#[allow(clippy::too_many_arguments)]
pub async fn send_asset_typed(
&self,
wallet: &Wallet,
source_dex: u32,
destination_dex: u32,
asset: u32,
destination: crate::wallet::Address,
amount: impl Into<String>,
to_perp: bool,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SendAsset {
metaflux_chain: chain,
source_dex,
destination_dex,
asset,
destination,
amount: amount.clone(),
to_perp,
nonce,
};
let params = json!({
"source_dex": source_dex,
"destination_dex": destination_dex,
"asset": asset,
"destination": destination,
"amount": amount,
"to_perp": to_perp,
});
(action, "send_asset", params)
})
.await
}
pub async fn usd_class_transfer_typed(
&self,
wallet: &Wallet,
ntl: impl Into<String>,
to_perp: bool,
) -> Result<Value, ClientError> {
let ntl = ntl.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UsdClassTransfer {
metaflux_chain: chain,
ntl: ntl.clone(),
to_perp,
nonce,
};
let params = json!({ "ntl": ntl, "to_perp": to_perp });
(action, "usd_class_transfer", params)
})
.await
}
pub async fn withdraw_typed(
&self,
wallet: &Wallet,
asset: u32,
amount: impl Into<String>,
destination_chain_id: u32,
use_cctp: bool,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::Withdraw {
metaflux_chain: chain,
asset,
amount: amount.clone(),
destination_chain_id,
use_cctp,
nonce,
};
let params = json!({
"asset": asset,
"amount": amount,
"destination_chain_id": destination_chain_id,
"use_cctp": use_cctp,
});
(action, "withdraw", params)
})
.await
}
pub async fn approve_agent_typed(
&self,
wallet: &Wallet,
agent_address: crate::wallet::Address,
agent_name: impl Into<String>,
expires_at_ms: u64,
) -> Result<Value, ClientError> {
let agent_name = agent_name.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::ApproveAgent {
metaflux_chain: chain,
agent_address,
agent_name: agent_name.clone(),
expires_at_ms,
nonce,
};
let mut params = json!({ "agent": agent_address, "name": agent_name });
if expires_at_ms != 0 {
params["expires_at_ms"] = json!(expires_at_ms);
}
(action, "approve_agent", params)
})
.await
}
pub async fn set_referrer_typed(
&self,
wallet: &Wallet,
referrer: crate::wallet::Address,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SetReferrer {
metaflux_chain: chain,
referrer,
nonce,
};
(action, "set_referrer", json!({ "referrer": referrer }))
})
.await
}
pub async fn approve_builder_fee_typed(
&self,
wallet: &Wallet,
builder: crate::wallet::Address,
max_fee_bps: u16,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::ApproveBuilderFee {
metaflux_chain: chain,
builder,
max_fee_bps,
nonce,
};
let params = json!({ "builder": builder, "max_bps": max_fee_bps });
(action, "approve_builder_fee", params)
})
.await
}
pub async fn set_display_name_typed(
&self,
wallet: &Wallet,
display_name: impl Into<String>,
) -> Result<Value, ClientError> {
let display_name = display_name.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SetDisplayName {
metaflux_chain: chain,
display_name: display_name.clone(),
nonce,
};
let params = json!({ "display_name": display_name });
(action, "set_display_name", params)
})
.await
}
pub async fn set_position_mode_typed(
&self,
wallet: &Wallet,
hedge: bool,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SetPositionMode {
metaflux_chain: chain,
hedge,
nonce,
};
(action, "set_position_mode", json!({ "hedge": hedge }))
})
.await
}
pub async fn user_portfolio_margin_typed(
&self,
wallet: &Wallet,
enroll: bool,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UserPortfolioMargin {
metaflux_chain: chain,
enroll,
nonce,
};
(action, "user_portfolio_margin", json!({ "enroll": enroll }))
})
.await
}
pub async fn convert_to_multi_sig_user_typed(
&self,
wallet: &Wallet,
signers: Vec<crate::wallet::Address>,
threshold: u32,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::ConvertToMultiSigUser {
metaflux_chain: chain,
signers: signers.clone(),
threshold,
nonce,
};
let params = json!({ "signers": signers, "threshold": threshold });
(action, "convert_to_multi_sig_user", params)
})
.await
}
pub async fn update_leverage_typed(
&self,
wallet: &Wallet,
asset: u32,
leverage: u32,
is_isolated: bool,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UpdateLeverage {
metaflux_chain: chain,
asset,
leverage,
is_isolated,
nonce,
};
let params =
json!({ "asset": asset, "leverage": leverage, "is_isolated": is_isolated });
(action, "update_leverage", params)
})
.await
}
pub async fn claim_rewards_typed(
&self,
wallet: &Wallet,
validator: crate::wallet::Address,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::ClaimRewards {
metaflux_chain: chain,
validator,
nonce,
};
let params = if validator == crate::wallet::Address::ZERO {
json!({})
} else {
json!({ "validator": validator })
};
(action, "claim_rewards", params)
})
.await
}
pub async fn link_staking_user_typed(
&self,
wallet: &Wallet,
target: crate::wallet::Address,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::LinkStakingUser {
metaflux_chain: chain,
target,
nonce,
};
(action, "link_staking_user", json!({ "target": target }))
})
.await
}
pub async fn create_vault_typed(
&self,
wallet: &Wallet,
name: impl Into<String>,
lock_period_secs: u64,
kind: u8,
) -> Result<Value, ClientError> {
let name = name.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CreateVault {
metaflux_chain: chain,
name: name.clone(),
lock_period_secs,
kind,
nonce,
};
let kind_tag = if kind == 1 { "Metaliquidity" } else { "User" };
let params = json!({
"name": name,
"lock_period_secs": lock_period_secs,
"kind": kind_tag,
});
(action, "create_vault", params)
})
.await
}
pub async fn vault_modify_typed(
&self,
wallet: &Wallet,
vault_id: u64,
new_name: impl Into<String>,
) -> Result<Value, ClientError> {
let new_name = new_name.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::VaultModify {
metaflux_chain: chain,
vault_id,
new_name: new_name.clone(),
nonce,
};
let params = json!({ "vault_id": vault_id, "new_name": new_name });
(action, "vault_modify", params)
})
.await
}
pub async fn spot_margin_close_typed(
&self,
wallet: &Wallet,
pair: u32,
limit_px: u64,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SpotMarginClose {
metaflux_chain: chain,
pair,
limit_px,
nonce,
};
let params = json!({ "pair": pair, "limit_px": limit_px });
(action, "spot_margin_close", params)
})
.await
}
pub async fn set_metaliquidity_set_typed(
&self,
wallet: &Wallet,
account: crate::wallet::Address,
allowed: bool,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SetMetaliquiditySet {
metaflux_chain: chain,
account,
allowed,
nonce,
};
let params = json!({ "address": account, "allowed": allowed });
(action, "set_metaliquidity_set", params)
})
.await
}
pub async fn register_metaliquidity_operator_typed(
&self,
wallet: &Wallet,
vault_id: u64,
operator: crate::wallet::Address,
allowed: bool,
expires_at_ms: u64,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::RegisterMetaliquidityOperator {
metaflux_chain: chain,
vault_id,
operator,
allowed,
expires_at_ms,
nonce,
};
let mut params = json!({
"vault_id": vault_id,
"operator": operator,
"allowed": allowed,
});
if expires_at_ms != 0 {
params["expires_at_ms"] = json!(expires_at_ms);
}
(action, "register_metaliquidity_operator", params)
})
.await
}
pub async fn update_isolated_margin_typed(
&self,
wallet: &Wallet,
asset: u32,
delta: impl Into<String>,
) -> Result<Value, ClientError> {
let delta = delta.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UpdateIsolatedMargin {
metaflux_chain: chain,
asset,
delta: delta.clone(),
nonce,
};
let params = json!({ "asset": asset, "delta": delta });
(action, "update_isolated_margin", params)
})
.await
}
pub async fn top_up_isolated_only_margin_typed(
&self,
wallet: &Wallet,
asset: u32,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::TopUpIsolatedOnlyMargin {
metaflux_chain: chain,
asset,
amount: amount.clone(),
nonce,
};
let params = json!({ "asset": asset, "amount": amount });
(action, "top_up_isolated_only_margin", params)
})
.await
}
pub async fn token_delegate_typed(
&self,
wallet: &Wallet,
validator: crate::wallet::Address,
amount: impl Into<String>,
is_undelegate: bool,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::TokenDelegate {
metaflux_chain: chain,
validator,
amount: amount.clone(),
is_undelegate,
nonce,
};
let params = json!({
"validator": validator,
"amount": amount,
"is_undelegate": is_undelegate,
});
(action, "token_delegate", params)
})
.await
}
pub async fn vault_transfer_typed(
&self,
wallet: &Wallet,
vault_id: u64,
deposit: bool,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::VaultTransfer {
metaflux_chain: chain,
vault_id,
deposit,
amount: amount.clone(),
nonce,
};
let params = json!({
"vault_id": vault_id,
"deposit": deposit,
"amount": amount,
});
(action, "vault_transfer", params)
})
.await
}
pub async fn vault_withdraw_typed(
&self,
wallet: &Wallet,
vault_id: u64,
shares: impl Into<String>,
) -> Result<Value, ClientError> {
let shares = shares.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::VaultWithdraw {
metaflux_chain: chain,
vault_id,
shares: shares.clone(),
nonce,
};
let params = json!({ "vault_id": vault_id, "shares": shares });
(action, "vault_withdraw", params)
})
.await
}
pub async fn spot_margin_deposit_typed(
&self,
wallet: &Wallet,
pair: u32,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SpotMarginDeposit {
metaflux_chain: chain,
pair,
amount: amount.clone(),
nonce,
};
let params = json!({ "pair": pair, "amount": amount });
(action, "spot_margin_deposit", params)
})
.await
}
pub async fn spot_margin_withdraw_typed(
&self,
wallet: &Wallet,
pair: u32,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SpotMarginWithdraw {
metaflux_chain: chain,
pair,
amount: amount.clone(),
nonce,
};
let params = json!({ "pair": pair, "amount": amount });
(action, "spot_margin_withdraw", params)
})
.await
}
pub async fn spot_margin_open_typed(
&self,
wallet: &Wallet,
pair: u32,
size: u64,
limit_px: u64,
borrow: impl Into<String>,
) -> Result<Value, ClientError> {
let borrow = borrow.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SpotMarginOpen {
metaflux_chain: chain,
pair,
size,
limit_px,
borrow: borrow.clone(),
nonce,
};
let params = json!({
"pair": pair,
"size": size,
"limit_px": limit_px,
"borrow": borrow,
});
(action, "spot_margin_open", params)
})
.await
}
pub async fn earn_deposit_typed(
&self,
wallet: &Wallet,
asset: u32,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::EarnDeposit {
metaflux_chain: chain,
asset,
amount: amount.clone(),
nonce,
};
let params = json!({ "asset": asset, "amount": amount });
(action, "earn_deposit", params)
})
.await
}
pub async fn earn_withdraw_typed(
&self,
wallet: &Wallet,
asset: u32,
shares: impl Into<String>,
) -> Result<Value, ClientError> {
let shares = shares.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::EarnWithdraw {
metaflux_chain: chain,
asset,
shares: shares.clone(),
nonce,
};
let params = json!({ "asset": asset, "shares": shares });
(action, "earn_withdraw", params)
})
.await
}
pub async fn agent_set_abstraction_typed(
&self,
wallet: &Wallet,
user: crate::wallet::Address,
kind: u8,
value: impl Into<String>,
) -> Result<Value, ClientError> {
let value = value.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::AgentSetAbstraction {
metaflux_chain: chain,
user,
kind,
value: value.clone(),
nonce,
};
let params = json!({ "user": user, "kind": kind, "value": value });
(action, "agent_set_abstraction", params)
})
.await
}
pub async fn mb_withdraw_typed(
&self,
wallet: &Wallet,
chain: crate::types::meta_bridge::MbChain,
asset: u32,
amount: u64,
dst_addr: impl Into<String>,
) -> Result<Value, ClientError> {
let dst_addr = dst_addr.into();
let chain_u8 = mb_chain_to_u8(chain);
let chain_name = mb_chain_name(chain);
self.post_signed_typed(wallet, |meta_chain, nonce| {
let action = TypedAction::MbWithdraw {
metaflux_chain: meta_chain,
chain: chain_u8,
asset,
amount,
dst_addr: dst_addr.clone(),
nonce,
};
let params = json!({
"chain": chain_name,
"asset": asset,
"amount": amount,
"dst_addr": dst_addr,
});
(action, "mb_withdraw", params)
})
.await
}
pub async fn core_evm_transfer_typed(
&self,
wallet: &Wallet,
amount: impl Into<String>,
to_evm: bool,
destination: crate::wallet::Address,
asset: u32,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CoreEvmTransfer {
metaflux_chain: chain,
amount: amount.clone(),
to_evm,
destination,
asset,
nonce,
};
let params = json!({
"amount": amount,
"to_evm": to_evm,
"destination": destination,
"asset": asset,
});
(action, "core_evm_transfer", params)
})
.await
}
pub async fn create_sub_account_typed(
&self,
wallet: &Wallet,
name: impl Into<String>,
explicit_index: Option<u32>,
shared_stp_group: bool,
) -> Result<Value, ClientError> {
let name = name.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CreateSubAccount {
metaflux_chain: chain,
name: name.clone(),
has_explicit_index: explicit_index.is_some(),
explicit_index: explicit_index.unwrap_or(0),
shared_stp_group,
nonce,
};
let mut params = json!({ "name": name, "shared_stp_group": shared_stp_group });
if let Some(idx) = explicit_index {
params["explicit_index"] = json!(idx);
}
(action, "create_sub_account", params)
})
.await
}
pub async fn sub_account_transfer_typed(
&self,
wallet: &Wallet,
sub_index: u32,
deposit: bool,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SubAccountTransfer {
metaflux_chain: chain,
sub_index,
deposit,
amount: amount.clone(),
nonce,
};
let params = json!({
"sub_index": sub_index,
"deposit": deposit,
"amount": amount,
});
(action, "sub_account_transfer", params)
})
.await
}
pub async fn sub_account_spot_transfer_typed(
&self,
wallet: &Wallet,
sub_index: u32,
token: u32,
deposit: bool,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SubAccountSpotTransfer {
metaflux_chain: chain,
sub_index,
token,
deposit,
amount: amount.clone(),
nonce,
};
let params = json!({
"sub_index": sub_index,
"token": token,
"deposit": deposit,
"amount": amount,
});
(action, "sub_account_spot_transfer", params)
})
.await
}
pub async fn c_deposit_typed(
&self,
wallet: &Wallet,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CDeposit {
metaflux_chain: chain,
amount: amount.clone(),
nonce,
};
(action, "c_deposit", json!({ "amount": amount }))
})
.await
}
pub async fn c_withdraw_typed(
&self,
wallet: &Wallet,
amount: impl Into<String>,
) -> Result<Value, ClientError> {
let amount = amount.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CWithdraw {
metaflux_chain: chain,
amount: amount.clone(),
nonce,
};
(action, "c_withdraw", json!({ "amount": amount }))
})
.await
}
pub async fn user_dex_abstraction_typed(
&self,
wallet: &Wallet,
enabled: bool,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UserDexAbstraction {
metaflux_chain: chain,
enabled,
nonce,
};
(
action,
"user_dex_abstraction",
json!({ "enabled": enabled }),
)
})
.await
}
pub async fn user_set_abstraction_typed(
&self,
wallet: &Wallet,
kind: u8,
value: impl Into<String>,
) -> Result<Value, ClientError> {
let value = value.into();
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::UserSetAbstraction {
metaflux_chain: chain,
kind,
value: value.clone(),
nonce,
};
let params = json!({ "kind": kind, "value": value });
(action, "user_set_abstraction", params)
})
.await
}
pub async fn priority_bid_typed(
&self,
wallet: &Wallet,
asset: u32,
bid_bps: u16,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::PriorityBid {
metaflux_chain: chain,
asset,
bid_bps,
nonce,
};
let params = json!({ "asset": asset, "bid_bps": bid_bps });
(action, "priority_bid", params)
})
.await
}
pub async fn cancel_all_orders_typed(
&self,
wallet: &Wallet,
asset: Option<u32>,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::CancelAllOrders {
metaflux_chain: chain,
has_asset: asset.is_some(),
asset: asset.unwrap_or(0),
nonce,
};
let params = match asset {
Some(a) => json!({ "asset": a }),
None => json!({}),
};
(action, "cancel_all_orders", params)
})
.await
}
pub async fn submit_encrypted_order_typed(
&self,
wallet: &Wallet,
ciphertext: Vec<u8>,
commitment: [u8; 32],
threshold: u8,
target_block: u64,
reveal_deadline_ms: u64,
) -> Result<Value, ClientError> {
self.post_signed_typed(wallet, |chain, nonce| {
let action = TypedAction::SubmitEncryptedOrder {
metaflux_chain: chain,
ciphertext: ciphertext.clone(),
commitment,
threshold,
target_block,
reveal_deadline_ms,
nonce,
};
let params = json!({
"ciphertext": ciphertext,
"commitment": commitment.to_vec(),
"threshold": threshold,
"target_block": target_block,
"reveal_deadline_ms": reveal_deadline_ms,
});
(action, "submit_encrypted_order", params)
})
.await
}
async fn post_signed_typed<F, R>(&self, wallet: &Wallet, build: F) -> Result<R, ClientError>
where
F: FnOnce(String, u64) -> (TypedAction, &'static str, Value),
R: serde::de::DeserializeOwned,
{
let nonce = next_nonce();
let chain = metaflux_chain_tag(MTF_CHAIN_ID).to_string();
let (typed, ty, params) = build(chain, nonce);
let digest = TypedActionDigest::new(&typed, MTF_CHAIN_ID).to_digest();
let signature = wallet.sign_digest(&digest)?.to_hex();
let action = json!({ "type": ty, "params": params });
let envelope = TypedSignedEnvelope {
action: &action,
nonce,
signature,
sig_scheme: "typed",
};
self.client.post_json("/exchange", &envelope).await
}
pub(crate) async fn post_typed_trade<R: serde::de::DeserializeOwned>(
&self,
wallet: &Wallet,
action: Value,
typed: TypedTradingAction<'_>,
) -> Result<R, ClientError> {
let nonce = next_nonce();
let digest = TypedTradingDigest::new(typed, MTF_CHAIN_ID, nonce).digest()?;
let signature = wallet.sign_digest(&digest)?.to_hex();
let envelope = TypedSignedEnvelope {
action: &action,
nonce,
signature,
sig_scheme: "typed",
};
self.client.post_json("/exchange", &envelope).await
}
}
#[doc(hidden)]
pub fn _typed_digest_for_test(action: &TypedAction) -> [u8; 32] {
TypedActionDigest::new(action, MTF_CHAIN_ID).to_digest()
}
#[doc(hidden)]
pub fn _typed_trade_digest_for_test(action: TypedTradingAction<'_>, nonce: u64) -> [u8; 32] {
TypedTradingDigest::new(action, MTF_CHAIN_ID, nonce)
.digest()
.expect("typed trade digest")
}