use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub const OP_TYPE_ASPENS: &str = "ASPENS";
pub const OP_WITHDRAW: &str = "WITHDRAW";
pub const OP_PLACE_ORDER: &str = "PLACE_ORDER";
pub const OP_CANCEL_ORDER: &str = "CANCEL_ORDER";
pub const OP_GET_MY_STATE: &str = "GET_MY_STATE";
pub const OP_GET_BOOK_STATE: &str = "GET_BOOK_STATE";
pub const OP_EXPORT_HISTORY: &str = "EXPORT_HISTORY";
pub const OP_GET_CONFIG: &str = "GET_CONFIG";
pub fn to_bytes32(s: &str) -> [u8; 32] {
let mut out = [0u8; 32];
let b = s.as_bytes();
let n = b.len().min(32);
out[..n].copy_from_slice(&b[..n]);
out
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DirectInstruction {
#[serde(rename = "opType", with = "hex32")]
pub op_type: [u8; 32],
#[serde(rename = "opCommand", with = "hex32")]
pub op_command: [u8; 32],
#[serde(rename = "message", with = "hexbytes")]
pub message: Vec<u8>,
}
impl DirectInstruction {
pub fn new(command: &str, payload_json: Vec<u8>) -> Self {
Self {
op_type: to_bytes32(OP_TYPE_ASPENS),
op_command: to_bytes32(command),
message: payload_json,
}
}
}
pub(crate) mod hex32 {
use super::*;
pub fn serialize<S: Serializer>(v: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("0x{}", hex::encode(v)))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
let s = String::deserialize(d)?;
let bytes =
hex::decode(s.strip_prefix("0x").unwrap_or(&s)).map_err(serde::de::Error::custom)?;
bytes
.try_into()
.map_err(|_| serde::de::Error::custom("expected 32 bytes"))
}
}
pub mod u64_string {
use super::*;
use serde::de::{Error as DeError, Unexpected};
pub fn serialize<S: Serializer>(v: &u64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&v.to_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
match serde_json::Value::deserialize(d)? {
serde_json::Value::String(s) => s
.parse::<u64>()
.map_err(|e| D::Error::custom(format!("u64 string {s:?}: {e}"))),
serde_json::Value::Number(n) => n.as_u64().ok_or_else(|| {
D::Error::invalid_value(Unexpected::Other(&n.to_string()), &"a u64")
}),
other => Err(D::Error::invalid_type(
Unexpected::Other(&other.to_string()),
&"a u64 as string or number",
)),
}
}
}
pub mod hexbytes {
use super::*;
pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("0x{}", hex::encode(v)))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let s = String::deserialize(d)?;
let body = s.strip_prefix("0x").unwrap_or(&s);
if body.is_empty() {
return Ok(Vec::new());
}
hex::decode(body).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hex32_str(s: &str) -> String {
format!("0x{}", hex::encode(to_bytes32(s)))
}
#[test]
fn to_bytes32_golden_vectors() {
assert_eq!(
hex32_str("ASPENS"),
"0x415350454e530000000000000000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("DEPOSIT"),
"0x4445504f53495400000000000000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("WITHDRAW"),
"0x5749544844524157000000000000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("PLACE_ORDER"),
"0x504c4143455f4f52444552000000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("CANCEL_ORDER"),
"0x43414e43454c5f4f524445520000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("GET_MY_STATE"),
"0x4745545f4d595f53544154450000000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("GET_BOOK_STATE"),
"0x4745545f424f4f4b5f5354415445000000000000000000000000000000000000"
);
assert_eq!(
hex32_str("EXPORT_HISTORY"),
"0x4558504f52545f484953544f5259000000000000000000000000000000000000"
);
}
#[test]
fn to_bytes32_truncates_past_32() {
let s = "X".repeat(40);
assert_eq!(to_bytes32(&s), [b'X'; 32]);
}
#[test]
fn direct_instruction_json_shape() {
let payload = br#"{"marketId":"m1"}"#.to_vec();
let di = DirectInstruction::new(OP_GET_BOOK_STATE, payload);
let v: serde_json::Value = serde_json::to_value(&di).unwrap();
assert_eq!(
v["opType"],
"0x415350454e530000000000000000000000000000000000000000000000000000"
);
assert_eq!(
v["opCommand"],
"0x4745545f424f4f4b5f5354415445000000000000000000000000000000000000"
);
assert_eq!(v["message"], "0x7b226d61726b65744964223a226d31227d");
let back: DirectInstruction = serde_json::from_value(v).unwrap();
assert_eq!(back, di);
assert_eq!(back.message, br#"{"marketId":"m1"}"#);
}
#[test]
fn empty_hexbytes_is_0x() {
let di = DirectInstruction::new(OP_PLACE_ORDER, Vec::new());
let v: serde_json::Value = serde_json::to_value(&di).unwrap();
assert_eq!(v["message"], "0x");
let back: DirectInstruction = serde_json::from_value(v).unwrap();
assert!(back.message.is_empty());
}
}
#[cfg(test)]
mod u64_string_tests {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Holder {
#[serde(with = "super::u64_string")]
id: u64,
}
const TRUE_ID: u64 = 173_852_891_691_592_598;
#[test]
fn serializes_as_a_quoted_string() {
let j = serde_json::to_string(&Holder { id: TRUE_ID }).unwrap();
assert_eq!(j, r#"{"id":"173852891691592598"}"#);
}
#[test]
fn round_trips_every_u64_exactly() {
for id in [
TRUE_ID,
6_755_360_711_411_187_334,
1 << 53,
(1 << 53) + 1,
u64::MAX,
0,
] {
let j = serde_json::to_string(&Holder { id }).unwrap();
let back: Holder = serde_json::from_str(&j).unwrap();
assert_eq!(back.id, id, "round trip lost precision for {id}");
}
}
#[test]
fn accepts_a_bare_number_for_compatibility() {
let h: Holder = serde_json::from_str(r#"{"id":42}"#).unwrap();
assert_eq!(h.id, 42);
}
#[test]
fn rejects_non_integers_and_overflow() {
for bad in [
r#"{"id":"1.5"}"#,
r#"{"id":1.5}"#,
r#"{"id":"-1"}"#,
r#"{"id":-1}"#,
r#"{"id":"abc"}"#,
r#"{"id":"18446744073709551616"}"#,
r#"{"id":null}"#,
] {
assert!(
serde_json::from_str::<Holder>(bad).is_err(),
"accepted {bad}"
);
}
}
}