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#[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(), derivation_origin: Some("https://xthyg-wyaaa-aaaak-ao2fa-cai.icp0.io".to_string()),
253 actions,
254 }
255 }
256}
257
258impl StrikeActionMetadata {
259 pub fn with_canister_id(mut self, canister_id: String) -> Self {
261 self.canister_id = canister_id;
262 self
263 }
264
265 pub fn with_derivation_origin(mut self, derivation_origin: Option<String>) -> Self {
267 self.derivation_origin = derivation_origin;
268 self
269 }
270
271 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}