use serde::{Deserialize, Serialize};
use super::channels::Channel;
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!(
TradeEventType {
OrderFilledV1,
OrderFilledV2,
OrdersMatchedV1,
OrdersMatchedV2,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TradesPayload {
#[serde(rename = "type")]
pub kind: TradeEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub side: Option<i64>,
#[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 metadata: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_hash: Option<Hex>,
#[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 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 payouts: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub settled_price: Option<String>,
}
event_type_enum!(
LifecycleEventType {
MarketPrepared,
NegRiskQuestionPrepared,
OutcomeReported,
EventPrepared,
ConditionResolved,
ConditionPreparation,
ConditionResolution,
TokenRegistered,
}
);
#[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_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>,
}
event_type_enum!(
ActivityEventType {
Redemption,
BinaryRedemption,
NegRiskRedemption,
PositionsRedeemed,
CollateralPositionSplit,
CollateralPositionsMerged,
CollateralPositionsConverted,
NegRiskPositionsConverted,
CtfPositionSplit,
CtfPositionsMerge,
CtfPayoutRedemption,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ActivityPayload {
#[serde(rename = "type")]
pub kind: ActivityEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amounts: Option<Vec<Hex>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initiator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payout: Option<Hex>,
}
event_type_enum!(
CollateralEventType {
Transfer,
Approval,
Wrapped,
Unwrapped,
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CollateralPayload {
#[serde(rename = "type")]
pub kind: CollateralEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Hex>,
}
event_type_enum!(
CombosEventType {
EventPrepared,
ResultReported,
PositionRedeemed,
ModulePositionsMerged,
ModulePositionsSplit,
HorizontalMerge,
HorizontalSplit,
PositionConverted,
ConditionResolved,
ResolutionPaused,
ResolutionUnpaused,
ResolverPaused,
ResolverUnpaused,
BridgePositionMinted,
BridgePositionsBurned,
LegacyCollateralSettled,
MigrationConditionRegistered,
MigrationResolved,
PositionMigrated,
CombinatorialConditionPrepared,
Compressed,
ConvertedToYesBasket,
Extracted,
Injected,
MergedFromYesBasket,
MergedOnCondition,
SplitOnCondition,
CombinatorialWrapped,
CombinatorialUnwrapped,
TransferSingle,
TransferBatch,
}
);
#[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 from: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operator: Option<Hex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Hex>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PricesPayload {
pub token_id: Hex,
pub price: f64,
pub timestamp_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Payload {
Trades(TradesPayload),
Oracle(OraclePayload),
Lifecycle(LifecyclePayload),
Activity(ActivityPayload),
Collateral(CollateralPayload),
Combos(CombosPayload),
Prices(PricesPayload),
Other(serde_json::Value),
}
impl Payload {
pub(crate) fn from_channel(channel: Channel, data: serde_json::Value) -> Self {
fn typed<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),
}
}
match channel {
Channel::Trades | Channel::LargeTrades => typed(data, Payload::Trades),
Channel::Oracle => typed(data, Payload::Oracle),
Channel::Lifecycle => typed(data, Payload::Lifecycle),
Channel::Activity => typed(data, Payload::Activity),
Channel::Collateral => typed(data, Payload::Collateral),
Channel::Combos => typed(data, Payload::Combos),
Channel::Prices => typed(data, Payload::Prices),
Channel::Global | Channel::Wallets | Channel::Markets => {
serde_json::from_value(data.clone()).unwrap_or(Payload::Other(data))
}
}
}
}
#[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 prices_is_a_flat_tick() {
let data = json!({"token_id":"0x1","price":0.42,"timestamp_ms":1700000000000i64});
match Payload::from_channel(Channel::Prices, data) {
Payload::Prices(p) => {
assert_eq!(p.token_id, "0x1");
assert!((p.price - 0.42).abs() < f64::EPSILON);
}
other => panic!("expected prices, got {other:?}"),
}
}
#[test]
fn global_firehose_discriminates_by_type() {
let data = json!({"type":"market_prepared","conditionId":"0xc"});
assert!(matches!(
Payload::from_channel(Channel::Global, 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::Trades, data),
Payload::Other(_)
));
}
}