use wasm_bindgen::prelude::*;
use crate::{
app_data::{
cid::{appdata_hex_to_cid, cid_to_appdata_hex},
hash::{appdata_hex, stringify_deterministic},
ipfs::{get_app_data_info, validate_app_data_doc},
types::AppDataDoc,
},
config::{
chain::SupportedChainId,
contracts::{settlement_contract, vault_relayer},
},
order_signing::{
eip712::{domain_separator, order_hash, signing_digest},
types::UnsignedOrder,
utils::{compute_order_uid, sign_order},
},
types::{EcdsaSigningScheme, OrderKind, TokenBalance},
};
fn to_js_err(e: impl core::fmt::Display) -> JsValue {
JsValue::from_str(&e.to_string())
}
fn parse_order(json: &str) -> Result<UnsignedOrder, JsValue> {
let v: serde_json::Value = serde_json::from_str(json).map_err(to_js_err)?;
let parse_addr = |key: &str| -> Result<alloy_primitives::Address, JsValue> {
v.get(key)
.and_then(|s| s.as_str())
.ok_or_else(|| to_js_err(format!("missing field: {key}")))?
.parse()
.map_err(to_js_err)
};
let parse_u256 = |key: &str| -> Result<alloy_primitives::U256, JsValue> {
let s = v
.get(key)
.and_then(|s| s.as_str())
.ok_or_else(|| to_js_err(format!("missing field: {key}")))?;
s.parse().map_err(to_js_err)
};
let kind_str = v.get("kind").and_then(|s| s.as_str()).unwrap_or_else(|| "sell");
let kind = match kind_str {
"buy" => OrderKind::Buy,
_ => OrderKind::Sell,
};
let sell_token_balance_str =
v.get("sellTokenBalance").and_then(|s| s.as_str()).unwrap_or_else(|| "erc20");
let sell_token_balance = match sell_token_balance_str {
"external" => TokenBalance::External,
"internal" => TokenBalance::Internal,
_ => TokenBalance::Erc20,
};
let buy_token_balance_str =
v.get("buyTokenBalance").and_then(|s| s.as_str()).unwrap_or_else(|| "erc20");
let buy_token_balance = match buy_token_balance_str {
"external" => TokenBalance::External,
"internal" => TokenBalance::Internal,
_ => TokenBalance::Erc20,
};
let valid_to = v.get("validTo").and_then(|n| n.as_u64()).unwrap_or_else(|| 0) as u32;
let app_data_str = v
.get("appData")
.and_then(|s| s.as_str())
.unwrap_or_else(|| "0x0000000000000000000000000000000000000000000000000000000000000000");
let app_data: alloy_primitives::B256 = app_data_str.parse().map_err(to_js_err)?;
let partially_fillable =
v.get("partiallyFillable").and_then(|b| b.as_bool()).unwrap_or_else(|| false);
Ok(UnsignedOrder {
sell_token: parse_addr("sellToken")?,
buy_token: parse_addr("buyToken")?,
receiver: parse_addr("receiver").unwrap_or_else(|_| alloy_primitives::Address::ZERO),
sell_amount: parse_u256("sellAmount")?,
buy_amount: parse_u256("buyAmount")?,
valid_to,
app_data,
fee_amount: parse_u256("feeAmount").unwrap_or_else(|_| alloy_primitives::U256::ZERO),
kind,
partially_fillable,
sell_token_balance,
buy_token_balance,
})
}
fn hex_b256(b: alloy_primitives::B256) -> String {
format!("0x{}", alloy_primitives::hex::encode(b.as_slice()))
}
#[wasm_bindgen(js_name = "domainSeparator")]
#[must_use]
pub fn wasm_domain_separator(chain_id: u32) -> String {
hex_b256(domain_separator(u64::from(chain_id)))
}
#[wasm_bindgen(js_name = "orderHash")]
pub fn wasm_order_hash(order_json: &str) -> Result<String, JsValue> {
let order = parse_order(order_json)?;
Ok(hex_b256(order_hash(&order)))
}
#[wasm_bindgen(js_name = "signingDigest")]
pub fn wasm_signing_digest(domain_sep_hex: &str, order_hash_hex: &str) -> Result<String, JsValue> {
let ds: alloy_primitives::B256 = domain_sep_hex.parse().map_err(to_js_err)?;
let oh: alloy_primitives::B256 = order_hash_hex.parse().map_err(to_js_err)?;
Ok(hex_b256(signing_digest(ds, oh)))
}
#[wasm_bindgen(js_name = "computeOrderUid")]
pub fn wasm_compute_order_uid(
chain_id: u32,
order_json: &str,
owner: &str,
) -> Result<String, JsValue> {
let order = parse_order(order_json)?;
let owner_addr: alloy_primitives::Address = owner.parse().map_err(to_js_err)?;
Ok(compute_order_uid(u64::from(chain_id), &order, owner_addr))
}
#[wasm_bindgen(js_name = "signOrder")]
pub async fn wasm_sign_order(
order_json: &str,
chain_id: u32,
private_key: &str,
scheme: &str,
) -> Result<String, JsValue> {
let order = parse_order(order_json)?;
let signer: alloy_signer_local::PrivateKeySigner = private_key.parse().map_err(to_js_err)?;
let ecdsa_scheme = match scheme {
"ethsign" => EcdsaSigningScheme::EthSign,
_ => EcdsaSigningScheme::Eip712,
};
let result =
sign_order(&order, u64::from(chain_id), &signer, ecdsa_scheme).await.map_err(to_js_err)?;
let json = serde_json::json!({
"signature": result.signature,
"signingScheme": result.signing_scheme.as_str(),
});
serde_json::to_string(&json).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "signOrderWithBrowserWallet")]
pub async fn wasm_sign_order_with_browser_wallet(
order_json: &str,
chain_id: u32,
signer_fn: &js_sys::Function,
) -> Result<String, JsValue> {
let order = parse_order(order_json)?;
let chain = u64::from(chain_id);
let domain_sep = domain_separator(chain);
let o_hash = order_hash(&order);
let digest = signing_digest(domain_sep, o_hash);
let domain_sep_hex = hex_b256(domain_sep);
let order_hash_hex = hex_b256(o_hash);
let digest_hex = hex_b256(digest);
let promise =
signer_fn.call1(&JsValue::NULL, &JsValue::from_str(&digest_hex)).map_err(|e| {
to_js_err(format!("signer_fn call failed: {}", e.as_string().unwrap_or_default()))
})?;
let future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise));
let signature = future.await.map_err(|e| {
to_js_err(format!("signer rejected: {}", e.as_string().unwrap_or_default()))
})?;
let sig_str = signature
.as_string()
.ok_or_else(|| to_js_err("signer_fn must return a hex string signature"))?;
let json = serde_json::json!({
"signature": sig_str,
"signingScheme": "eip712",
"orderHash": order_hash_hex,
"domainSeparator": domain_sep_hex,
"signingDigest": digest_hex,
});
serde_json::to_string(&json).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "appdataHex")]
pub fn wasm_appdata_hex(doc_json: &str) -> Result<String, JsValue> {
let doc: AppDataDoc = serde_json::from_str(doc_json).map_err(to_js_err)?;
let hash = appdata_hex(&doc).map_err(to_js_err)?;
Ok(hex_b256(hash))
}
#[wasm_bindgen(js_name = "stringifyDeterministic")]
pub fn wasm_stringify_deterministic(doc_json: &str) -> Result<String, JsValue> {
let doc: AppDataDoc = serde_json::from_str(doc_json).map_err(to_js_err)?;
stringify_deterministic(&doc).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "getAppDataInfo")]
pub fn wasm_get_app_data_info(doc_json: &str) -> Result<String, JsValue> {
let doc: AppDataDoc = serde_json::from_str(doc_json).map_err(to_js_err)?;
let info = get_app_data_info(&doc).map_err(to_js_err)?;
let json = serde_json::json!({
"cid": info.cid,
"appDataContent": info.app_data_content,
"appDataHex": info.app_data_hex,
});
serde_json::to_string(&json).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "validateAppDataDoc")]
pub fn wasm_validate_app_data_doc(doc_json: &str) -> Result<String, JsValue> {
let doc: AppDataDoc = serde_json::from_str(doc_json).map_err(to_js_err)?;
let result = validate_app_data_doc(&doc);
let json = serde_json::json!({
"success": result.success,
"errors": result.errors,
});
serde_json::to_string(&json).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "appdataHexToCid")]
pub fn wasm_appdata_hex_to_cid(app_data_hex: &str) -> Result<String, JsValue> {
appdata_hex_to_cid(app_data_hex).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "cidToAppdataHex")]
pub fn wasm_cid_to_appdata_hex(cid: &str) -> Result<String, JsValue> {
cid_to_appdata_hex(cid).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "settlementContract")]
pub fn wasm_settlement_contract(chain_id: u32) -> Result<String, JsValue> {
let chain = SupportedChainId::try_from_u64(u64::from(chain_id))
.ok_or_else(|| to_js_err(format!("unsupported chain ID: {chain_id}")))?;
Ok(format!("{:#x}", settlement_contract(chain)))
}
#[wasm_bindgen(js_name = "vaultRelayer")]
pub fn wasm_vault_relayer(chain_id: u32) -> Result<String, JsValue> {
let chain = SupportedChainId::try_from_u64(u64::from(chain_id))
.ok_or_else(|| to_js_err(format!("unsupported chain ID: {chain_id}")))?;
Ok(format!("{:#x}", vault_relayer(chain)))
}
#[wasm_bindgen(js_name = "apiBaseUrl")]
pub fn wasm_api_base_url(chain_id: u32, env: &str) -> Result<String, JsValue> {
let chain = SupportedChainId::try_from_u64(u64::from(chain_id))
.ok_or_else(|| to_js_err(format!("unsupported chain ID: {chain_id}")))?;
let environment = match env {
"staging" => crate::config::chain::Env::Staging,
_ => crate::config::chain::Env::Prod,
};
Ok(crate::config::chain::api_base_url(chain, environment).to_owned())
}
#[wasm_bindgen(js_name = "supportedChainIds")]
#[must_use]
pub fn wasm_supported_chain_ids() -> String {
let ids: Vec<u64> = SupportedChainId::all().iter().map(|c| c.as_u64()).collect();
serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_owned())
}
#[allow(clippy::type_complexity, reason = "tuple return matches domain parse requirements")]
fn parse_chain_env(
chain_id: u32,
env: &str,
) -> Result<(SupportedChainId, crate::config::chain::Env), JsValue> {
let chain = SupportedChainId::try_from_u64(u64::from(chain_id))
.ok_or_else(|| to_js_err(format!("unsupported chain ID: {chain_id}")))?;
let environment = match env {
"staging" => crate::config::chain::Env::Staging,
_ => crate::config::chain::Env::Prod,
};
Ok((chain, environment))
}
#[wasm_bindgen(js_name = "getQuote")]
pub async fn wasm_get_quote(
chain_id: u32,
env: &str,
request_json: &str,
) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let req: crate::order_book::OrderQuoteRequest =
serde_json::from_str(request_json).map_err(to_js_err)?;
let resp = api.get_quote(&req).await.map_err(to_js_err)?;
serde_json::to_string(&resp).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "sendOrder")]
pub async fn wasm_send_order(
chain_id: u32,
env: &str,
order_creation_json: &str,
) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let creation: crate::order_book::OrderCreation =
serde_json::from_str(order_creation_json).map_err(to_js_err)?;
let uid = api.send_order(&creation).await.map_err(to_js_err)?;
serde_json::to_string(&uid).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "getOrder")]
pub async fn wasm_get_order(chain_id: u32, env: &str, order_uid: &str) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let order = api.get_order(order_uid).await.map_err(to_js_err)?;
serde_json::to_string(&order).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "getTrades")]
pub async fn wasm_get_trades(chain_id: u32, env: &str, order_uid: &str) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let trades = api.get_trades(Some(order_uid), None).await.map_err(to_js_err)?;
serde_json::to_string(&trades).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "getOrdersByOwner")]
pub async fn wasm_get_orders_by_owner(
chain_id: u32,
env: &str,
owner: &str,
) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let owner_addr: alloy_primitives::Address = owner.parse().map_err(to_js_err)?;
let orders = api.get_orders_for_account(owner_addr, None).await.map_err(to_js_err)?;
serde_json::to_string(&orders).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "getNativePrice")]
pub async fn wasm_get_native_price(
chain_id: u32,
env: &str,
token: &str,
) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::order_book::OrderBookApi::new(chain, environment);
let token_addr: alloy_primitives::Address = token.parse().map_err(to_js_err)?;
let price = api.get_native_price(token_addr).await.map_err(to_js_err)?;
Ok(price.to_string())
}
#[wasm_bindgen(js_name = "getSubgraphTotals")]
pub async fn wasm_get_subgraph_totals(chain_id: u32, env: &str) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let api = crate::subgraph::SubgraphApi::new(chain, environment).map_err(to_js_err)?;
let totals = api.get_totals().await.map_err(to_js_err)?;
serde_json::to_string(&totals).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "fetchDocFromCid")]
pub async fn wasm_fetch_doc_from_cid(
cid: &str,
ipfs_uri: Option<String>,
) -> Result<String, JsValue> {
let doc =
crate::app_data::fetch_doc_from_cid(cid, ipfs_uri.as_deref()).await.map_err(to_js_err)?;
serde_json::to_string(&doc).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "fetchDocFromAppDataHex")]
pub async fn wasm_fetch_doc_from_app_data_hex(
app_data_hex: &str,
ipfs_uri: Option<String>,
) -> Result<String, JsValue> {
let doc = crate::app_data::fetch_doc_from_app_data_hex(app_data_hex, ipfs_uri.as_deref())
.await
.map_err(to_js_err)?;
serde_json::to_string(&doc).map_err(to_js_err)
}
#[wasm_bindgen(js_name = "postSwapOrder")]
pub async fn wasm_post_swap_order(
chain_id: u32,
env: &str,
app_code: &str,
private_key: &str,
params_json: &str,
) -> Result<String, JsValue> {
let (chain, environment) = parse_chain_env(chain_id, env)?;
let config = match environment {
crate::config::chain::Env::Staging => {
crate::trading::TradingSdkConfig::staging(chain, app_code)
}
_ => crate::trading::TradingSdkConfig::prod(chain, app_code),
};
let sdk = crate::trading::TradingSdk::new(config, private_key).map_err(to_js_err)?;
let v: serde_json::Value = serde_json::from_str(params_json).map_err(to_js_err)?;
let parse_addr = |key: &str| -> Result<alloy_primitives::Address, JsValue> {
v.get(key)
.and_then(|s| s.as_str())
.ok_or_else(|| to_js_err(format!("missing field: {key}")))?
.parse()
.map_err(to_js_err)
};
let kind_str = v.get("kind").and_then(|s| s.as_str()).unwrap_or_else(|| "sell");
let kind = match kind_str {
"buy" => OrderKind::Buy,
_ => OrderKind::Sell,
};
let amount_str = v
.get("amount")
.and_then(|s| s.as_str())
.ok_or_else(|| to_js_err("missing field: amount"))?;
let amount: alloy_primitives::U256 = amount_str.parse().map_err(to_js_err)?;
let params = crate::trading::TradeParameters {
kind,
sell_token: parse_addr("sellToken")?,
sell_token_decimals: v
.get("sellTokenDecimals")
.and_then(|n| n.as_u64())
.unwrap_or_else(|| 18) as u8,
buy_token: parse_addr("buyToken")?,
buy_token_decimals: v.get("buyTokenDecimals").and_then(|n| n.as_u64()).unwrap_or_else(|| 18)
as u8,
amount,
slippage_bps: v.get("slippageBps").and_then(|n| n.as_u64()).map(|n| n as u32),
receiver: None,
valid_for: None,
valid_to: None,
partially_fillable: None,
partner_fee: None,
};
let result = sdk.post_swap_order(params).await.map_err(to_js_err)?;
let json = serde_json::json!({
"orderId": result.order_id,
"signingScheme": result.signing_scheme.as_str(),
"signature": result.signature,
});
serde_json::to_string(&json).map_err(to_js_err)
}