use serde::{Deserialize, Serialize};
use super::wire::hexbytes;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaceOrderRequest {
pub side: String,
pub quantity: String,
#[serde(rename = "price", skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(rename = "marketId")]
pub market_id: String,
#[serde(rename = "baseAccountAddress")]
pub base_account_address: String,
#[serde(rename = "quoteAccountAddress")]
pub quote_account_address: String,
#[serde(rename = "executionType", skip_serializing_if = "Option::is_none")]
pub execution_type: Option<String>,
#[serde(rename = "postOnly", skip_serializing_if = "Option::is_none")]
pub post_only: Option<bool>,
#[serde(rename = "signatureHash", with = "hexbytes")]
pub signature_hash: Vec<u8>,
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "amountIn")]
pub amount_in: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaceOrderResponse {
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "orderInBook")]
pub order_in_book: bool,
pub fills: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelOrderRequest {
#[serde(rename = "marketId")]
pub market_id: String,
pub side: String,
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "signatureHash", with = "hexbytes")]
pub signature_hash: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelOrderResponse {
pub canceled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawRequest {
pub network: String,
pub token: String,
pub account: String,
pub amount: String,
#[serde(with = "hexbytes")]
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawVoucher {
pub account: String,
pub token: String,
pub amount: String,
#[serde(with = "crate::fce::wire::u64_string")]
pub nonce: u64,
#[serde(with = "crate::fce::wire::u64_string")]
pub expiry: u64,
#[serde(with = "hexbytes")]
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMyStateRequest {
#[serde(rename = "marketId")]
pub market_id: String,
pub trader: String,
}
fn null_as_empty<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
let opt = Option::<Vec<T>>::deserialize(de)?;
Ok(opt.unwrap_or_default())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMyStateResponse {
#[serde(rename = "openOrders", default, deserialize_with = "null_as_empty")]
pub open_orders: Vec<OpenOrder>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenOrder {
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "marketId")]
pub market_id: String,
pub side: String,
pub price: String,
pub quantity: String,
pub state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBookStateRequest {
#[serde(rename = "marketId")]
pub market_id: String,
pub depth: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBookStateResponse {
#[serde(rename = "marketId")]
pub market_id: String,
#[serde(default, deserialize_with = "null_as_empty")]
pub bids: Vec<BookLevel>,
#[serde(default, deserialize_with = "null_as_empty")]
pub asks: Vec<BookLevel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BookLevel {
pub price: String,
pub quantity: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportHistoryRequest {
#[serde(rename = "marketId")]
pub market_id: String,
pub trader: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportHistoryResponse {
#[serde(default, deserialize_with = "null_as_empty")]
pub trades: Vec<TradeRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeRecord {
#[serde(with = "crate::fce::wire::u64_string")]
pub timestamp: u64,
pub price: String,
pub quantity: String,
#[serde(rename = "orderHit")]
pub order_hit: String,
#[serde(rename = "buyerIs", default)]
pub buyer_is: String,
#[serde(rename = "sellerIs", default)]
pub seller_is: String,
#[serde(rename = "makerBaseAddress", default)]
pub maker_base_address: String,
#[serde(rename = "makerQuoteAddress", default)]
pub maker_quote_address: String,
#[serde(rename = "takerBaseAddress", default)]
pub taker_base_address: String,
#[serde(rename = "takerQuoteAddress", default)]
pub taker_quote_address: String,
}
#[cfg(test)]
mod trade_record_tests {
use super::*;
#[test]
fn decodes_the_roles_and_addresses_the_adapter_sends() {
let json = r#"{
"timestamp": 1785870359683,
"price": "1000000",
"quantity": "2000000",
"orderHit": "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"buyerIs": "TAKER",
"sellerIs": "MAKER",
"makerBaseAddress": "0xmb",
"makerQuoteAddress": "0xmq",
"takerBaseAddress": "0xtb",
"takerQuoteAddress": "0xtq"
}"#;
let t: TradeRecord = serde_json::from_str(json).expect("decode");
assert_eq!(t.buyer_is, "TAKER");
assert_eq!(t.seller_is, "MAKER");
assert_eq!(t.maker_base_address, "0xmb");
assert_eq!(t.taker_base_address, "0xtb");
}
#[test]
fn tolerates_an_adapter_that_omits_the_new_fields() {
let json = r#"{"timestamp":1,"price":"1","quantity":"1","orderHit":"0x02"}"#;
let t: TradeRecord = serde_json::from_str(json).expect("decode");
assert_eq!(t.buyer_is, "");
assert_eq!(t.maker_base_address, "");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn place_order_field_names_match_adapter() {
let req = PlaceOrderRequest {
side: "BID".into(),
quantity: "5".into(),
price: Some("1000".into()),
market_id: "m".into(),
base_account_address: "0xbase".into(),
quote_account_address: "0xquote".into(),
execution_type: None,
post_only: None,
signature_hash: vec![0xab, 0xcd],
order_id: "0xoid".into(),
amount_in: "5".into(),
};
let v = serde_json::to_value(&req).unwrap();
for k in [
"side",
"quantity",
"price",
"marketId",
"baseAccountAddress",
"quoteAccountAddress",
"signatureHash",
"orderId",
"amountIn",
] {
assert!(v.get(k).is_some(), "missing field {k}");
}
assert!(v.get("executionType").is_none());
assert!(v.get("postOnly").is_none());
assert_eq!(v["signatureHash"], "0xabcd");
}
#[test]
fn market_order_omits_price() {
let req = PlaceOrderRequest {
side: "ASK".into(),
quantity: "1".into(),
price: None,
market_id: "m".into(),
base_account_address: "0xb".into(),
quote_account_address: "0xq".into(),
execution_type: None,
post_only: None,
signature_hash: vec![],
order_id: "0x".into(),
amount_in: "1".into(),
};
let v = serde_json::to_value(&req).unwrap();
assert!(v.get("price").is_none());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GetConfigRequest {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetConfigEnvelope {
#[serde(rename = "configProto", with = "crate::fce::wire::hexbytes")]
pub config_proto: Vec<u8>,
}
#[cfg(test)]
mod config_tests {
use super::*;
use crate::commands::config::config_pb::GetConfigResponse;
use prost::Message as _;
#[test]
fn envelope_round_trips_into_the_generated_config_type() {
let original = GetConfigResponse::default();
let hex_body = hex::encode(original.encode_to_vec());
let json = format!(r#"{{"configProto":"0x{hex_body}"}}"#);
let env: GetConfigEnvelope = serde_json::from_str(&json).expect("decode envelope");
let decoded =
GetConfigResponse::decode(env.config_proto.as_slice()).expect("decode protobuf");
assert_eq!(decoded, original);
}
#[test]
fn non_utf8_protobuf_bytes_survive_the_round_trip() {
let raw = vec![0x0a, 0x03, 0xff, 0xfe, 0xfd, 0x12, 0x00];
let json = format!(r#"{{"configProto":"0x{}"}}"#, hex::encode(&raw));
let env: GetConfigEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(env.config_proto, raw);
}
#[test]
fn envelope_field_is_camel_case_config_proto() {
let env = GetConfigEnvelope {
config_proto: vec![0x00],
};
let json = serde_json::to_string(&env).unwrap();
assert!(json.contains(r#""configProto""#), "got {json}");
}
#[test]
fn a_malformed_payload_is_an_error_not_an_empty_config() {
let r: Result<GetConfigEnvelope, _> = serde_json::from_str(r#"{"configProto":"0xnothex"}"#);
assert!(r.is_err(), "malformed hex must not decode");
}
}