Skip to main content

strike_actions/
lib.rs

1use std::fmt::Display;
2
3use candid::types::TypeInner;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7#[derive(Serialize, Deserialize, Debug)]
8#[serde(rename_all = "lowercase")]
9pub enum ActionType {
10    Query,
11    Update,
12}
13
14#[derive(Serialize, Deserialize, Debug)]
15#[serde(rename_all = "camelCase")]
16pub struct UIParameter {
17    pub name: String,
18    pub label: String,
19    pub candid_type: CandidType,
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23#[serde(rename_all = "camelCase")]
24pub struct StrikeAction {
25    pub label: String,
26    pub method: String,
27    #[serde(rename = "type")]
28    pub action_type: ActionType,
29    pub ui_parameters: Vec<UIParameter>,
30    pub input: Vec<CandidType>,
31    pub input_parameters: Vec<Value>,
32    pub output: Vec<CandidType>,
33}
34
35#[derive(Serialize, Deserialize, Debug)]
36#[serde(rename_all = "camelCase")]
37pub struct StrikeActionMetadata {
38    pub icon: String,
39    pub homepage: String,
40    pub label: String,
41    pub title: String,
42    pub description: String,
43    pub canister_id: String,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub derivation_origin: Option<String>,
46    pub actions: Vec<StrikeAction>,
47}
48
49#[derive(Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)]
50pub struct CandidType(TypeInner);
51
52impl From<TypeInner> for CandidType {
53    fn from(value: TypeInner) -> Self {
54        CandidType(value)
55    }
56}
57
58impl Display for CandidType {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        let output = match &self.0 {
61            TypeInner::Null => "Null".to_string(),
62            TypeInner::Bool => "Bool".to_string(),
63            TypeInner::Nat => "Nat".to_string(),
64            TypeInner::Int => "Int".to_string(),
65            TypeInner::Nat8 => "Nat8".to_string(),
66            TypeInner::Nat16 => "Nat16".to_string(),
67            TypeInner::Nat32 => "Nat32".to_string(),
68            TypeInner::Nat64 => "Nat64".to_string(),
69            TypeInner::Int8 => "Int8".to_string(),
70            TypeInner::Int16 => "Int16".to_string(),
71            TypeInner::Int32 => "Int32".to_string(),
72            TypeInner::Int64 => "Int64".to_string(),
73            TypeInner::Float32 => "Float32".to_string(),
74            TypeInner::Float64 => "Float64".to_string(),
75            TypeInner::Text => "Text".to_string(),
76            TypeInner::Reserved => "Reserved".to_string(),
77            TypeInner::Empty => "Empty".to_string(),
78            TypeInner::Knot(_) => "Knot".to_string(),
79            TypeInner::Var(_) => "Var".to_string(),
80            TypeInner::Unknown => "Unknown".to_string(),
81            TypeInner::Opt(_) => "Opt".to_string(),
82            TypeInner::Vec(_) => "Vec".to_string(),
83            TypeInner::Record(_) => "Record".to_string(),
84            TypeInner::Variant(_) => "Variant".to_string(),
85            TypeInner::Func(_) => "Func".to_string(),
86            TypeInner::Service(_) => "Service".to_string(),
87            TypeInner::Class(_, _) => "Class".to_string(),
88            TypeInner::Principal => "Principal".to_string(),
89            TypeInner::Future => "Future".to_string(),
90        };
91        write!(f, "{}", output)
92    }
93}
94
95impl Serialize for CandidType {
96    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97    where
98        S: serde::Serializer,
99    {
100        self.to_string().serialize(serializer)
101    }
102}
103
104impl<'de> Deserialize<'de> for CandidType {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        let s = String::deserialize(deserializer)?;
110        let type_inner = match s.as_str() {
111            "Null" => TypeInner::Null,
112            "Bool" => TypeInner::Bool,
113            "Nat" => TypeInner::Nat,
114            "Int" => TypeInner::Int,
115            "Nat8" => TypeInner::Nat8,
116            "Nat16" => TypeInner::Nat16,
117            "Nat32" => TypeInner::Nat32,
118            "Nat64" => TypeInner::Nat64,
119            "Int8" => TypeInner::Int8,
120            "Int16" => TypeInner::Int16,
121            "Int32" => TypeInner::Int32,
122            "Int64" => TypeInner::Int64,
123            "Float32" => TypeInner::Float32,
124            "Float64" => TypeInner::Float64,
125            "Text" => TypeInner::Text,
126            "Reserved" => TypeInner::Reserved,
127            "Empty" => TypeInner::Empty,
128            "Principal" => TypeInner::Principal,
129            "Future" => TypeInner::Future,
130            _ => return Err(serde::de::Error::custom(format!("Unknown CandidType: {}", s))),
131        };
132        Ok(CandidType(type_inner))
133    }
134}
135
136// Simplified market types for the library
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct MarketOption {
139    pub option: String,
140    pub yes_token_amount: u128,
141    pub no_token_amount: u128,
142    pub yes_bet_amount: u128,
143    pub no_bet_amount: u128,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Market {
148    pub id: String,
149    pub title: String,
150    pub description: String,
151    pub image: String,
152    pub options: Vec<MarketOption>,
153}
154
155impl From<Market> for StrikeActionMetadata {
156    fn from(value: Market) -> Self {
157        let actions = if value.options.len() == 1 {
158            let option = value.options.first().unwrap();
159
160            vec![
161                StrikeAction {
162                    label: format!("Bet Yes on {}", option.option.clone()),
163                    method: "bet".to_string(),
164                    action_type: ActionType::Update,
165                    ui_parameters: vec![UIParameter {
166                        name: "bet_amount".to_string(),
167                        label: "Bet amount".to_string(),
168                        candid_type: CandidType::from(TypeInner::Nat),
169                    }],
170                    input: vec![
171                        CandidType::from(TypeInner::Text),
172                        CandidType::from(TypeInner::Nat32),
173                        CandidType::from(TypeInner::Text),
174                        CandidType::from(TypeInner::Nat),
175                    ],
176                    input_parameters: vec![json!(value.id.clone()), json!(1), json!("Yes"), json!("{bet_amount}")],
177                    output: vec![CandidType::from(TypeInner::Text)],
178                },
179                StrikeAction {
180                    label: format!("Bet No on {}", option.option.clone()),
181                    method: "bet".to_string(),
182                    action_type: ActionType::Update,
183                    ui_parameters: vec![UIParameter {
184                        name: "bet_amount".to_string(),
185                        label: "Bet amount".to_string(),
186                        candid_type: CandidType::from(TypeInner::Nat),
187                    }],
188                    input: vec![
189                        CandidType::from(TypeInner::Text),
190                        CandidType::from(TypeInner::Nat32),
191                        CandidType::from(TypeInner::Text),
192                        CandidType::from(TypeInner::Nat),
193                    ],
194                    input_parameters: vec![json!(value.id.clone()), json!(1), json!("No"), json!("{bet_amount}")],
195                    output: vec![CandidType::from(TypeInner::Text)],
196                },
197            ]
198        } else {
199            value
200                .options
201                .into_iter()
202                .enumerate()
203                .fold(vec![], |mut acc, (option_id, option)| {
204                    acc.push(StrikeAction {
205                        label: format!("Bet Yes on {}", option.option.clone()),
206                        method: "bet".to_string(),
207                        action_type: ActionType::Update,
208                        ui_parameters: vec![UIParameter {
209                            name: "bet_amount".to_string(),
210                            label: "Bet amount".to_string(),
211                            candid_type: CandidType::from(TypeInner::Nat),
212                        }],
213                        input: vec![
214                            CandidType::from(TypeInner::Text),
215                            CandidType::from(TypeInner::Nat32),
216                            CandidType::from(TypeInner::Text),
217                            CandidType::from(TypeInner::Nat),
218                        ],
219                        input_parameters: vec![json!(value.id.clone()), json!(option_id), json!("Yes"), json!("{bet_amount}")],
220                        output: vec![CandidType::from(TypeInner::Text)],
221                    });
222                    acc.push(StrikeAction {
223                        label: format!("Bet No on {}", option.option.clone()),
224                        method: "bet".to_string(),
225                        action_type: ActionType::Update,
226                        ui_parameters: vec![UIParameter {
227                            name: "bet_amount".to_string(),
228                            label: "Bet amount".to_string(),
229                            candid_type: CandidType::from(TypeInner::Nat),
230                        }],
231                        input: vec![
232                            CandidType::from(TypeInner::Text),
233                            CandidType::from(TypeInner::Nat32),
234                            CandidType::from(TypeInner::Text),
235                            CandidType::from(TypeInner::Nat),
236                        ],
237                        input_parameters: vec![json!(value.id.clone()), json!(option_id), json!("No"), json!("{bet_amount}")],
238                        output: vec![CandidType::from(TypeInner::Text)],
239                    });
240
241                    acc
242                })
243        };
244
245        StrikeActionMetadata {
246            homepage: format!("https://betbtc.win/market/{}", value.id),
247            icon: value.image,
248            label: "betBTC".to_string(),
249            title: value.title,
250            description: value.description,
251            canister_id: "default-canister-id".to_string(), // Default canister ID
252            derivation_origin: Some("https://xthyg-wyaaa-aaaak-ao2fa-cai.icp0.io".to_string()),
253            actions,
254        }
255    }
256}
257
258impl StrikeActionMetadata {
259    /// Create StrikeActionMetadata with a custom canister ID
260    pub fn with_canister_id(mut self, canister_id: String) -> Self {
261        self.canister_id = canister_id;
262        self
263    }
264
265    /// Create StrikeActionMetadata with a custom derivation origin
266    pub fn with_derivation_origin(mut self, derivation_origin: Option<String>) -> Self {
267        self.derivation_origin = derivation_origin;
268        self
269    }
270
271    /// Create StrikeActionMetadata with a custom label
272    pub fn with_label(mut self, label: String) -> Self {
273        self.label = label;
274        self
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_into_strike_action_metadata() {
284        let market = Market {
285            id: "market1".to_string(),
286            title: "Test Market".to_string(),
287            description: "A test market".to_string(),
288            image: "test-image.png".to_string(),
289            options: vec![
290                MarketOption {
291                    option: "Option 1".to_string(),
292                    yes_token_amount: 0,
293                    no_token_amount: 0,
294                    yes_bet_amount: 0,
295                    no_bet_amount: 0,
296                },
297                MarketOption {
298                    option: "Option 2".to_string(),
299                    yes_token_amount: 0,
300                    no_token_amount: 0,
301                    yes_bet_amount: 0,
302                    no_bet_amount: 0,
303                },
304            ],
305        };
306
307        let strike_action_metadata: StrikeActionMetadata = market.clone().into();
308
309        let strike_json = serde_json::to_string(&strike_action_metadata).unwrap();
310
311        assert!(strike_json.contains("Bet Yes on Option 1"));
312        assert!(strike_json.contains("Bet No on Option 1"));
313        assert!(strike_json.contains("Bet Yes on Option 2"));
314        assert!(strike_json.contains("Bet No on Option 2"));
315        assert!(strike_json.contains("betBTC"));
316        assert!(strike_json.contains("Test Market"));
317    }
318
319    #[test]
320    fn test_candid_type_serialization() {
321        let candid_type = CandidType::from(TypeInner::Nat);
322        let serialized = serde_json::to_string(&candid_type).unwrap();
323        assert_eq!(serialized, "\"Nat\"");
324    }
325
326    #[test]
327    fn test_candid_type_deserialization() {
328        let json = "\"Nat\"";
329        let candid_type: CandidType = serde_json::from_str(json).unwrap();
330        assert_eq!(candid_type.to_string(), "Nat");
331    }
332
333    #[test]
334    fn test_with_canister_id() {
335        let market = Market {
336            id: "market1".to_string(),
337            title: "Test Market".to_string(),
338            description: "A test market".to_string(),
339            image: "test-image.png".to_string(),
340            options: vec![MarketOption {
341                option: "Option 1".to_string(),
342                yes_token_amount: 0,
343                no_token_amount: 0,
344                yes_bet_amount: 0,
345                no_bet_amount: 0,
346            }],
347        };
348
349        let strike_action_metadata: StrikeActionMetadata =
350            StrikeActionMetadata::from(market).with_canister_id("custom-canister-id".to_string());
351
352        assert_eq!(strike_action_metadata.canister_id, "custom-canister-id");
353    }
354}