use serde::{Deserialize, Serialize};
use super::channels::{Channel, ClobChannel};
pub type Hex = String;
macro_rules! event_type_enum {
($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident),* $(,)? }) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
#[allow(missing_docs, clippy::enum_variant_names)]
pub enum $name {
$($(#[$vmeta])* $variant,)*
}
};
}
event_type_enum!(
TradingEventType {
OrderFilledV1,
OrderFilledV2,
OrdersMatchedV1,
OrdersMatchedV2,
OrderCancelled,
OrderPreapproved,
OrderPreapprovalInvalidated,
TradingPaused,
TradingUnpaused,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TradingPayload {
#[serde(rename = "type")]
pub kind: TradingEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub builder: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maker: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maker_amount_filled: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maker_asset_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_hash: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pauser: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub side: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taker: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taker_amount_filled: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taker_asset_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taker_order_hash: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taker_order_maker: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_id: Option<Hex>,
}
event_type_enum!(
FeesEventType {
FeeChargedV1,
FeeChargedV2,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FeesPayload {
#[serde(rename = "type")]
pub kind: FeesEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receiver: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_id: Option<Hex>,
}
event_type_enum!(
OracleEventType {
UmaAdapterQuestionInitialized,
UmaAdapterQuestionResolved,
UmaAdapterQuestionEmergencyResolved,
UmaAdapterQuestionFlagged,
UmaAdapterQuestionPaused,
UmaAdapterQuestionUnpaused,
UmaAdapterQuestionReset,
UmaAdapterAncillaryDataUpdated,
UmaOptimisticQuestionInitialized,
UmaOptimisticQuestionResolved,
UmaOptimisticQuestionPaused,
UmaOptimisticQuestionUnpaused,
UmaOptimisticQuestionSettled,
UmaOptimisticResolutionDataRequested,
UmaOptimisticQuestionUpdated,
UmaOptimisticQuestionFlaggedForAdminResolution,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OraclePayload {
#[serde(rename = "type")]
pub kind: OracleEventType,
#[serde(rename = "questionID", skip_serializing_if = "Option::is_none")]
pub question_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ancillary_data: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub early_resolution: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub early_resolution_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub emergency_report: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payouts: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proposal_bond: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_timestamp: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requestor: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reward: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reward_token: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_time: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub settled_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update: Option<Hex>,
}
event_type_enum!(
ResolutionEventType {
ConditionResolution,
ConditionResolved,
OutcomeReported,
ResultReported,
ResolutionPaused,
ResolutionUnpaused,
ResolverPaused,
ResolverUnpaused,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResolutionPayload {
#[serde(rename = "type")]
pub kind: ResolutionEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oracle: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome_slot_count: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payout_numerators: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub question_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolver: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<Hex>,
}
event_type_enum!(
LifecycleEventType {
MarketPrepared,
EventPrepared,
ConditionPreparation,
TokenRegistered,
NegRiskQuestionPrepared,
CombinatorialConditionPrepared,
MigrationConditionRegistered,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LifecyclePayload {
#[serde(rename = "type")]
pub kind: LifecycleEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_count: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_bips: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_event_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legs: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oracle: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome_slot_count: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub question_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token0: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token1: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v2_condition_id: Option<Hex>,
}
event_type_enum!(
PositionsEventType {
CtfPositionSplit,
CtfPositionsMerge,
CtfPayoutRedemption,
CollateralPositionSplit,
CollateralPositionsMerged,
PositionsRedeemed,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionsPayload {
#[serde(rename = "type")]
pub kind: PositionsEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amounts: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collateral_token: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_sets: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initiator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_collection_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partition: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payout: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redeemer: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stakeholder: Option<Hex>,
}
event_type_enum!(
CombosEventType {
Redemption,
BinaryRedemption,
NegRiskRedemption,
CollateralPositionsConverted,
NegRiskPositionsConverted,
PositionConverted,
PositionRedeemed,
ModulePositionsMerged,
ModulePositionsSplit,
HorizontalMerge,
HorizontalSplit,
SplitOnCondition,
MergedOnCondition,
ConvertedToYesBasket,
MergedFromYesBasket,
Extracted,
Injected,
Compressed,
CombinatorialWrapped,
CombinatorialUnwrapped,
PositionMigrated,
MigrationResolved,
BridgePositionMinted,
BridgePositionsBurned,
LegacyCollateralSettled,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CombosPayload {
#[serde(rename = "type")]
pub kind: CombosEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_out: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amounts: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub child_no_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub child_yes_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collateral_out: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub combinatorial_position_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_index: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub full_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_set: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initiator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_payout0: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_payout1: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_token: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_position_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_position_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome_index: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payout: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_ids: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient0: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient1: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduced_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub residual_condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result0: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result1: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stakeholder: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub underlying_position_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vault: Option<Hex>,
}
event_type_enum!(
TransfersEventType {
TransferSingle,
TransferBatch,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TransfersPayload {
#[serde(rename = "type")]
pub kind: TransfersEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub operator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ids: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amounts: Option<Vec<Hex>>,
}
event_type_enum!(
AccountsEventType {
WalletDeployed,
ProxyCreation,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountsPayload {
#[serde(rename = "type")]
pub kind: AccountsEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub wallet: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implementation: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<Hex>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum OrderSide {
Buy,
Sell,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MempoolOrder {
pub maker: String,
pub taker: Option<String>,
pub token_id: String,
pub side: OrderSide,
pub maker_amount: String,
pub taker_amount: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MempoolCall {
pub method: String,
#[serde(default)]
pub market_ids: Vec<String>,
#[serde(default)]
pub token_ids: Vec<String>,
#[serde(default)]
pub wallets: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notional_usd: Option<f64>,
#[serde(default)]
pub orders: Vec<MempoolOrder>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MempoolPayload {
pub seen_at_ms: u64,
pub transaction_hash: String,
pub from: String,
pub to: String,
#[serde(default)]
pub contract_kinds: Vec<String>,
#[serde(default)]
pub method_selector: Option<String>,
#[serde(default)]
pub call: Option<MempoolCall>,
pub input: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Level {
pub price: f64,
pub size: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobBookPayload {
pub asset_id: String,
pub market: String,
pub timestamp: i64,
pub bids: Vec<Level>,
pub asks: Vec<Level>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobLastTradePayload {
pub asset_id: String,
pub market: String,
pub price: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PriceChange {
pub asset_id: String,
pub price: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub best_bid: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub best_ask: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobPricesPayload {
pub market: String,
pub timestamp: i64,
pub changes: Vec<PriceChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobMidpointPayload {
pub asset_id: String,
pub market: String,
pub midpoint: f64,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobTickSizePayload {
pub asset_id: String,
pub market: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClobBestBidAskPayload {
pub asset_id: String,
pub market: String,
pub best_bid: f64,
pub best_ask: f64,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Payload {
Trading(TradingPayload),
Fees(FeesPayload),
Oracle(OraclePayload),
Resolution(ResolutionPayload),
Lifecycle(Box<LifecyclePayload>),
Positions(PositionsPayload),
Combos(Box<CombosPayload>),
Transfers(TransfersPayload),
Accounts(AccountsPayload),
Mempool(MempoolPayload),
ClobBook(ClobBookPayload),
ClobPrices(ClobPricesPayload),
ClobLastTrade(ClobLastTradePayload),
ClobMidpoint(ClobMidpointPayload),
ClobTickSize(ClobTickSizePayload),
ClobBestBidAsk(ClobBestBidAskPayload),
Other(serde_json::Value),
}
fn typed_payload<T, F>(data: serde_json::Value, wrap: F) -> Payload
where
T: for<'de> Deserialize<'de>,
F: FnOnce(T) -> Payload,
{
match serde_json::from_value::<T>(data.clone()) {
Ok(value) => wrap(value),
Err(_) => Payload::Other(data),
}
}
impl Payload {
pub(crate) fn from_channel(channel: Channel, data: serde_json::Value) -> Self {
match channel {
Channel::Trading => typed_payload(data, Payload::Trading),
Channel::Fees => typed_payload(data, Payload::Fees),
Channel::Oracle => typed_payload(data, Payload::Oracle),
Channel::Resolution => typed_payload(data, Payload::Resolution),
Channel::Lifecycle => typed_payload(data, Payload::Lifecycle),
Channel::Positions => typed_payload(data, Payload::Positions),
Channel::Combos => typed_payload(data, Payload::Combos),
Channel::Transfers => typed_payload(data, Payload::Transfers),
Channel::Accounts => typed_payload(data, Payload::Accounts),
Channel::Wallets | Channel::Markets => {
serde_json::from_value(data.clone()).unwrap_or(Payload::Other(data))
}
}
}
pub(crate) fn from_pending(data: serde_json::Value) -> Self {
typed_payload(data, Payload::Mempool)
}
pub(crate) fn from_clob_channel(channel: ClobChannel, data: serde_json::Value) -> Self {
match channel {
ClobChannel::Book => typed_payload(data, Payload::ClobBook),
ClobChannel::Prices => typed_payload(data, Payload::ClobPrices),
ClobChannel::LastTrade => typed_payload(data, Payload::ClobLastTrade),
ClobChannel::Midpoint => typed_payload(data, Payload::ClobMidpoint),
ClobChannel::TickSize => typed_payload(data, Payload::ClobTickSize),
ClobChannel::BestBidAsk => typed_payload(data, Payload::ClobBestBidAsk),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn oracle_renames_question_id() {
let data = json!({"type":"uma_optimistic_question_resolved","questionID":"0xq","settledPrice":"-1"});
match Payload::from_channel(Channel::Oracle, data) {
Payload::Oracle(o) => {
assert_eq!(o.kind, OracleEventType::UmaOptimisticQuestionResolved);
assert_eq!(o.question_id.as_deref(), Some("0xq"));
assert_eq!(o.settled_price.as_deref(), Some("-1"));
}
other => panic!("expected oracle, got {other:?}"),
}
}
#[test]
fn trading_types_a_fill() {
let data = json!({"type":"order_filled_v2","side":1,"tokenId":"0xabc"});
match Payload::from_channel(Channel::Trading, data) {
Payload::Trading(t) => {
assert_eq!(t.kind, TradingEventType::OrderFilledV2);
assert_eq!(t.side, Some(1));
assert_eq!(t.token_id.as_deref(), Some("0xabc"));
}
other => panic!("expected trading, got {other:?}"),
}
}
#[test]
fn wallets_view_discriminates_by_type() {
let data = json!({"type":"market_prepared","conditionId":"0xc"});
assert!(matches!(
Payload::from_channel(Channel::Wallets, data),
Payload::Lifecycle(_)
));
}
#[test]
fn unknown_event_type_falls_back_to_other() {
let data = json!({"type":"brand_new_event","foo":1});
assert!(matches!(
Payload::from_channel(Channel::Trading, data),
Payload::Other(_)
));
}
fn pending_envelope(call: serde_json::Value) -> serde_json::Value {
json!({
"seen_at_ms": 1_782_027_489_000u64,
"transaction_hash": "0xhash",
"from": "0xfrom",
"to": "0xto",
"contract_kinds": ["exchange"],
"method_selector": "0xabcdef12",
"call": call,
"input": "0xdead",
"value": "0",
})
}
#[test]
fn pending_types_a_decoded_transaction() {
let data = pending_envelope(json!({
"method": "fillOrders",
"market_ids": ["0xm"],
"token_ids": ["7"],
"wallets": ["0xw"],
"notional_usd": 192.5,
"orders": [{
"maker": "0xa",
"taker": null,
"token_id": "7",
"side": "sell",
"maker_amount": "100",
"taker_amount": "50",
}],
}));
match Payload::from_pending(data) {
Payload::Mempool(tx) => {
assert_eq!(tx.transaction_hash, "0xhash");
assert_eq!(tx.from, "0xfrom");
assert_eq!(tx.contract_kinds, vec!["exchange".to_string()]);
let call = tx.call.expect("call present");
assert_eq!(call.method, "fillOrders");
assert_eq!(call.market_ids, vec!["0xm".to_string()]);
assert_eq!(call.notional_usd, Some(192.5));
assert_eq!(call.orders.len(), 1);
assert_eq!(call.orders[0].side, OrderSide::Sell);
assert_eq!(call.orders[0].taker, None);
assert_eq!(call.orders[0].maker_amount, "100");
}
other => panic!("expected mempool, got {other:?}"),
}
}
#[test]
fn pending_non_trade_call_has_empty_orders() {
let data = pending_envelope(json!({"method": "splitPosition", "notional_usd": null}));
match Payload::from_pending(data) {
Payload::Mempool(tx) => {
let call = tx.call.expect("call present");
assert!(call.orders.is_empty());
assert!(call.notional_usd.is_none());
assert!(call.token_ids.is_empty());
}
other => panic!("expected mempool, got {other:?}"),
}
}
#[test]
fn pending_undecodable_call_is_null() {
let data = pending_envelope(serde_json::Value::Null);
match Payload::from_pending(data) {
Payload::Mempool(tx) => assert!(tx.call.is_none()),
other => panic!("expected mempool, got {other:?}"),
}
}
#[test]
fn pending_without_envelope_falls_back_to_other() {
let data = json!({"foo": 1});
assert!(matches!(Payload::from_pending(data), Payload::Other(_)));
}
#[test]
fn clob_book_types_a_snapshot() {
let data = json!({
"asset_id": "123",
"market": "0xabc",
"timestamp": 1_700_000_000_i64,
"bids": [{"price": 0.4, "size": 100.0}],
"asks": [{"price": 0.6, "size": 50.0}],
});
match Payload::from_clob_channel(ClobChannel::Book, data) {
Payload::ClobBook(book) => {
assert_eq!(book.asset_id, "123");
assert_eq!(book.market, "0xabc");
assert_eq!(book.bids.len(), 1);
assert_eq!(book.bids[0].price, 0.4);
assert_eq!(book.asks[0].size, 50.0);
}
other => panic!("expected clob book, got {other:?}"),
}
}
#[test]
fn clob_last_trade_size_is_optional() {
let data = json!({"asset_id":"9","market":"0xm","price":0.55,"timestamp":42});
match Payload::from_clob_channel(ClobChannel::LastTrade, data) {
Payload::ClobLastTrade(trade) => {
assert_eq!(trade.price, 0.55);
assert!(trade.size.is_none());
assert_eq!(trade.timestamp, 42);
}
other => panic!("expected clob last_trade, got {other:?}"),
}
}
#[test]
fn clob_payload_mismatch_falls_back_to_other() {
let data = json!({"asset_id":"1","market":"0xm","timestamp":1});
assert!(matches!(
Payload::from_clob_channel(ClobChannel::Book, data),
Payload::Other(_)
));
}
}