1use std::{
6 collections::{BTreeMap, BTreeSet, btree_map::Entry},
7 fmt,
8 str::FromStr,
9};
10
11use crate::utils::sign::{L1Action, Map, Value};
12use crate::utils::time::Stamp;
13use crate::utils::{Address, Dec};
14
15#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
18pub enum DeployError {
19 #[error("`{0}` is no venue name: a venue is 2 to 4 lower case letters")]
20 Venue(String),
21 #[error("`spot` is the spot venue's name and cannot be claimed")]
22 SpotVenue,
23 #[error("a fee scale is a decimal from 0 to 10, which `{0}` is not")]
24 FeeScale(String),
25 #[error("a settle fraction is a decimal from 0 to 1, which `{0}` is not")]
26 Fraction(String),
27 #[error("a question needs at least one named outcome")]
28 NoNamedOutcomes,
29 #[error("setting sub-deployers needs at least one grant")]
30 NoGrants,
31 #[error("settling a question needs each of its named outcomes not yet settled")]
32 NoSettlements,
33 #[error("outcome {0} belongs to a question, which settles to 0 or 1 only")]
34 NotWhole(u32),
35 #[error("a question settles with exactly one named outcome at 1, not {0}")]
36 Winners(usize),
37 #[error("outcome {0} is settled twice")]
38 SettledTwice(u32),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct Venue(String);
46
47impl Venue {
48 #[must_use]
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54impl FromStr for Venue {
55 type Err = DeployError;
56
57 fn from_str(name: &str) -> Result<Self, Self::Err> {
59 let letters = name.bytes().all(|byte| byte.is_ascii_lowercase());
60 if !(2..=4).contains(&name.len()) || !letters {
61 return Err(DeployError::Venue(name.to_owned()));
62 }
63 if name == "spot" {
64 return Err(DeployError::SpotVenue);
65 }
66 Ok(Venue(name.to_owned()))
67 }
68}
69
70impl fmt::Display for Venue {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76fn bounded(text: &str, most: Dec) -> Option<Dec> {
79 let text = text.trim();
80 if text.starts_with(['+', '-']) {
81 return None;
82 }
83 let value = Dec::parse(text).ok()?;
84 (!value.is_negative() && value <= most).then_some(value)
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct FeeScale(Dec);
93
94impl FeeScale {
95 pub const ZERO: FeeScale = FeeScale(Dec::ZERO);
96 pub const ONE: FeeScale = FeeScale(Dec::ONE);
97
98 #[must_use]
99 pub const fn value(self) -> Dec {
100 self.0
101 }
102
103 #[must_use]
105 pub fn to_wire(self) -> String {
106 self.0.to_wire()
107 }
108}
109
110impl FromStr for FeeScale {
111 type Err = DeployError;
112
113 fn from_str(text: &str) -> Result<Self, Self::Err> {
116 bounded(text, Dec::from_int(10)).map(FeeScale).ok_or_else(|| DeployError::FeeScale(text.trim().to_owned()))
117 }
118}
119
120impl fmt::Display for FeeScale {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "{}", self.0)
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct Fraction(Dec);
131
132impl Fraction {
133 pub const ZERO: Fraction = Fraction(Dec::ZERO);
134 pub const ONE: Fraction = Fraction(Dec::ONE);
135
136 #[must_use]
137 pub const fn value(self) -> Dec {
138 self.0
139 }
140
141 #[must_use]
142 pub fn to_wire(self) -> String {
143 self.0.to_wire()
144 }
145
146 #[must_use]
148 pub fn is_whole(self) -> bool {
149 self == Fraction::ZERO || self == Fraction::ONE
150 }
151}
152
153impl FromStr for Fraction {
154 type Err = DeployError;
155
156 fn from_str(text: &str) -> Result<Self, Self::Err> {
158 bounded(text, Dec::ONE).map(Fraction).ok_or_else(|| DeployError::Fraction(text.trim().to_owned()))
159 }
160}
161
162impl fmt::Display for Fraction {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "{}", self.0)
165 }
166}
167
168pub const MAX_VALUE_LEN: usize = 100;
170
171pub const MAX_SHORT_LEN: usize = 10;
173
174pub const HORIZON_MS: u64 = 366 * 24 * 60 * 60 * 1000;
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum Problem {
180 Missing,
181 TooLong,
182 Reserved,
185 DateTime,
186 Date,
187 ShortString,
188 UInt,
189 TooLarge,
191 UDecimal,
192 Passed,
194 TooFar,
196 Unknown,
198 Repeated,
199}
200
201impl Problem {
202 #[must_use]
204 pub const fn words(self) -> &'static str {
205 match self {
206 Problem::Missing => "needs a value",
207 Problem::TooLong => "takes at most 100 characters",
208 Problem::Reserved => "cannot hold {, } or |",
209 Problem::DateTime => "takes YYYYMMDD-HHMM in UTC",
210 Problem::Date => "takes YYYYMMDD in UTC",
211 Problem::ShortString => "takes at most 10 characters",
212 Problem::UInt => "takes a whole number with no sign or leading zeros",
213 Problem::TooLarge => "takes a whole number that fits in 64 bits",
214 Problem::UDecimal => "takes a decimal with no sign, no exponent and no leading or trailing zeros",
215 Problem::Passed => "has already passed",
216 Problem::TooFar => "is more than a year away",
217 Problem::Unknown => "is not a keyword of this template",
218 Problem::Repeated => "is given twice",
219 }
220 }
221}
222
223impl fmt::Display for Problem {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(self.words())
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
231#[error("{keyword} {problem}")]
232pub struct BadValue {
233 pub keyword: String,
234 pub problem: Problem,
235}
236
237impl BadValue {
238 fn new(keyword: &str, problem: Problem) -> Self {
239 BadValue { keyword: keyword.to_owned(), problem }
240 }
241}
242
243fn plain(keyword: &str, value: &str) -> Result<(), BadValue> {
246 let problem = if value.is_empty() {
247 Problem::Missing
248 } else if value.len() > MAX_VALUE_LEN {
249 Problem::TooLong
250 } else if value.contains(['{', '}', '|']) {
251 Problem::Reserved
252 } else {
253 return Ok(());
254 };
255 Err(BadValue::new(keyword, problem))
256}
257
258fn is_uint(text: &str) -> bool {
260 let digits = !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
261 digits && (text == "0" || !text.starts_with('0'))
262}
263
264fn is_udecimal(text: &str) -> bool {
266 let (whole, fraction) = match text.split_once('.') {
267 Some((whole, fraction)) => (whole, Some(fraction)),
268 None => (text, None),
269 };
270 is_uint(whole)
271 && fraction.is_none_or(|fraction| {
272 !fraction.is_empty() && fraction.bytes().all(|byte| byte.is_ascii_digit()) && !fraction.ends_with('0')
273 })
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Hash)]
279pub enum Hint {
280 DateTime,
282 Date,
284 ShortString,
286 HlPerp,
289 UInt,
291 UDecimal,
293 String,
295 Other(String),
298}
299
300impl Hint {
301 #[must_use]
303 pub fn parse(name: &str) -> Hint {
304 match name {
305 "dateTime" => Hint::DateTime,
306 "date" => Hint::Date,
307 "shortString" => Hint::ShortString,
308 "hlPerp" => Hint::HlPerp,
309 "uInt" => Hint::UInt,
310 "uDecimal" => Hint::UDecimal,
311 "string" => Hint::String,
312 other => Hint::Other(other.to_owned()),
313 }
314 }
315
316 #[must_use]
318 pub fn name(&self) -> &str {
319 match self {
320 Hint::DateTime => "dateTime",
321 Hint::Date => "date",
322 Hint::ShortString => "shortString",
323 Hint::HlPerp => "hlPerp",
324 Hint::UInt => "uInt",
325 Hint::UDecimal => "uDecimal",
326 Hint::String => "string",
327 Hint::Other(name) => name,
328 }
329 }
330
331 pub fn check(&self, keyword: &str, value: &str) -> Result<(), BadValue> {
334 plain(keyword, value)?;
335 let problem = match self {
336 Hint::DateTime if Stamp::exact(value).is_none() => Problem::DateTime,
337 Hint::Date if Stamp::day(value).is_none() => Problem::Date,
338 Hint::ShortString if value.len() > MAX_SHORT_LEN => Problem::ShortString,
339 Hint::UInt if !is_uint(value) => Problem::UInt,
340 Hint::UInt if value.parse::<u64>().is_err() => Problem::TooLarge,
341 Hint::UDecimal if !is_udecimal(value) => Problem::UDecimal,
342 _ => return Ok(()),
343 };
344 Err(BadValue::new(keyword, problem))
345 }
346
347 pub fn check_at(&self, keyword: &str, value: &str, now_ms: u64) -> Result<(), BadValue> {
351 self.check(keyword, value)?;
352 if *self != Hint::DateTime {
353 return Ok(());
354 }
355 let Some(stamp) = Stamp::exact(value) else {
356 return Err(BadValue::new(keyword, Problem::DateTime));
357 };
358 let at = i128::from(stamp.ms());
359 let now = i128::from(now_ms);
360 let problem = if at <= now {
361 Problem::Passed
362 } else if at > now + i128::from(HORIZON_MS) {
363 Problem::TooFar
364 } else {
365 return Ok(());
366 };
367 Err(BadValue::new(keyword, problem))
368 }
369}
370
371impl fmt::Display for Hint {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 f.write_str(self.name())
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Default)]
381pub struct Keywords(BTreeMap<String, String>);
382
383impl Keywords {
384 #[must_use]
386 pub const fn new() -> Self {
387 Keywords(BTreeMap::new())
388 }
389
390 pub fn from_pairs<K: Into<String>, V: Into<String>>(
392 pairs: impl IntoIterator<Item = (K, V)>,
393 ) -> Result<Self, BadValue> {
394 pairs.into_iter().try_fold(Keywords::new(), |keywords, (keyword, value)| keywords.with(keyword, value))
395 }
396
397 pub fn with(mut self, keyword: impl Into<String>, value: impl Into<String>) -> Result<Self, BadValue> {
399 let keyword = keyword.into();
400 let value = value.into();
401 plain(&keyword, &value)?;
402 match self.0.entry(keyword) {
403 Entry::Occupied(taken) => Err(BadValue::new(taken.key(), Problem::Repeated)),
404 Entry::Vacant(free) => {
405 free.insert(value);
406 Ok(self)
407 }
408 }
409 }
410
411 #[must_use]
412 pub fn get(&self, keyword: &str) -> Option<&str> {
413 self.0.get(keyword).map(String::as_str)
414 }
415
416 #[must_use]
417 pub fn len(&self) -> usize {
418 self.0.len()
419 }
420
421 #[must_use]
422 pub fn is_empty(&self) -> bool {
423 self.0.is_empty()
424 }
425
426 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
428 self.0.iter().map(|(keyword, value)| (keyword.as_str(), value.as_str()))
429 }
430
431 #[must_use]
433 pub fn wire(&self) -> Value {
434 Value::array(self.iter().map(|(keyword, value)| Value::array([keyword.to_owned(), value.to_owned()])))
435 }
436
437 #[must_use]
442 pub fn check<'a>(&self, declared: impl IntoIterator<Item = (&'a str, Hint)>, now_ms: u64) -> Vec<BadValue> {
443 let declared: BTreeMap<&str, Hint> = declared.into_iter().collect();
444 let mut problems = Vec::new();
445 for (keyword, hint) in &declared {
446 match self.get(keyword) {
447 None => problems.push(BadValue::new(keyword, Problem::Missing)),
448 Some(value) => problems.extend(hint.check_at(keyword, value, now_ms).err()),
449 }
450 }
451 for keyword in self.0.keys() {
452 if !declared.contains_key(keyword.as_str()) {
453 problems.push(BadValue::new(keyword, Problem::Unknown));
454 }
455 }
456 problems
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Eq)]
462pub struct Instance {
463 pub id: String,
465 pub keywords: Keywords,
466}
467
468impl Instance {
469 #[must_use]
470 pub fn new(id: impl Into<String>, keywords: Keywords) -> Self {
471 Instance { id: id.into(), keywords }
472 }
473
474 #[must_use]
475 pub fn wire(&self) -> Map {
476 Map::new().put("id", self.id.clone()).put("keywordToValue", self.keywords.wire())
477 }
478
479 fn wire_with(&self, fee_scale: FeeScale) -> Map {
481 self.wire().put("deployerFeeScale", fee_scale.to_wire())
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct Settlement {
491 pub outcome: u32,
492 pub fraction: Fraction,
493 pub name: String,
494 pub description: String,
495 pub sides: [String; 2],
497}
498
499impl Settlement {
500 #[must_use]
502 pub fn wire(&self) -> Map {
503 let [first, second] = &self.sides;
504 Map::new()
505 .put("outcome", self.outcome)
506 .put("settleFraction", self.fraction.to_wire())
507 .put("details", "")
508 .put("nameAndDescription", Value::array([self.name.clone(), self.description.clone()]))
509 .put("sideNames", Value::array([first.clone(), second.clone()]))
510 }
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum Permission {
517 RegisterStandalone,
518 RegisterQuestion,
519 RegisterAndAssociate,
520 SettleOutcome,
521 SettleQuestion,
523}
524
525impl Permission {
526 pub const ALL: [Permission; 5] = [
527 Permission::RegisterStandalone,
528 Permission::RegisterQuestion,
529 Permission::RegisterAndAssociate,
530 Permission::SettleOutcome,
531 Permission::SettleQuestion,
532 ];
533
534 #[must_use]
535 pub const fn wire(self) -> &'static str {
536 match self {
537 Permission::RegisterStandalone => "registerStandaloneOutcomeFromTemplate",
538 Permission::RegisterQuestion => "registerQuestionFromTemplate",
539 Permission::RegisterAndAssociate => "registerAndAssociateNamedOutcomeFromTemplate",
540 Permission::SettleOutcome => "settleOutcome",
541 Permission::SettleQuestion => "settleQuestion",
542 }
543 }
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
548pub struct Grant {
549 pub permission: Permission,
550 pub user: Address,
551 pub allowed: bool,
552}
553
554impl Grant {
555 #[must_use]
556 pub fn wire(&self) -> Map {
557 Map::new().put("variant", self.permission.wire()).put("user", self.user.to_hex()).put("allowed", self.allowed)
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
563pub enum Operation {
564 RegisterStandalone { instance: Instance, fee_scale: FeeScale },
566 RegisterQuestion { question: Instance, fee_scale: FeeScale, named: Vec<Instance> },
570 RegisterAndAssociate { question: u32, named: Instance },
573 SettleOutcome(Settlement),
575 SettleQuestion { question: u32, settlements: Vec<Settlement>, name: String, description: String },
579 SetSubDeployers(Vec<Grant>),
581}
582
583impl Operation {
584 #[must_use]
586 pub const fn wire_name(&self) -> &'static str {
587 match self {
588 Operation::RegisterStandalone { .. } => "registerStandaloneOutcomeFromTemplate",
589 Operation::RegisterQuestion { .. } => "registerQuestionFromTemplate",
590 Operation::RegisterAndAssociate { .. } => "registerAndAssociateNamedOutcomeFromTemplate",
591 Operation::SettleOutcome(_) => "settleOutcome",
592 Operation::SettleQuestion { .. } => "settleQuestion2",
593 Operation::SetSubDeployers(_) => "setSubDeployers",
594 }
595 }
596
597 #[must_use]
600 pub const fn permission(&self) -> Option<Permission> {
601 match self {
602 Operation::RegisterStandalone { .. } => Some(Permission::RegisterStandalone),
603 Operation::RegisterQuestion { .. } => Some(Permission::RegisterQuestion),
604 Operation::RegisterAndAssociate { .. } => Some(Permission::RegisterAndAssociate),
605 Operation::SettleOutcome(_) => Some(Permission::SettleOutcome),
606 Operation::SettleQuestion { .. } => Some(Permission::SettleQuestion),
607 Operation::SetSubDeployers(_) => None,
608 }
609 }
610
611 #[must_use]
613 pub const fn is_settlement(&self) -> bool {
614 matches!(self, Operation::SettleOutcome(_) | Operation::SettleQuestion { .. })
615 }
616
617 #[must_use]
618 pub fn body(&self) -> Value {
619 match self {
620 Operation::RegisterStandalone { instance, fee_scale } => instance.wire_with(*fee_scale).into(),
621 Operation::RegisterQuestion { question, fee_scale, named } => Map::new()
622 .put("questionTemplateInstance", question.wire_with(*fee_scale))
623 .put("namedOutcomeTemplateInstances", Value::array(named.iter().map(Instance::wire)))
624 .into(),
625 Operation::RegisterAndAssociate { question, named } => {
626 Map::new().put("question", *question).put("namedOutcomeTemplateInstance", named.wire()).into()
627 }
628 Operation::SettleOutcome(settlement) => settlement.wire().into(),
629 Operation::SettleQuestion { question, settlements, name, description } => Map::new()
630 .put("question", *question)
631 .put("outcomeSettlements", Value::array(settlements.iter().map(Settlement::wire)))
632 .put("nameAndDescription", Value::array([name.clone(), description.clone()]))
633 .into(),
634 Operation::SetSubDeployers(grants) => Value::array(grants.iter().map(Grant::wire)),
635 }
636 }
637}
638
639#[derive(Debug, Clone, PartialEq, Eq)]
647pub struct OutcomeDeploy {
648 pub venue: Venue,
649 pub operation: Operation,
650}
651
652impl OutcomeDeploy {
653 #[must_use]
654 pub fn register_standalone(venue: Venue, instance: Instance, fee_scale: FeeScale) -> Self {
655 OutcomeDeploy { venue, operation: Operation::RegisterStandalone { instance, fee_scale } }
656 }
657
658 pub fn register_question(
659 venue: Venue,
660 question: Instance,
661 fee_scale: FeeScale,
662 named: Vec<Instance>,
663 ) -> Result<Self, DeployError> {
664 if named.is_empty() {
665 return Err(DeployError::NoNamedOutcomes);
666 }
667 Ok(OutcomeDeploy { venue, operation: Operation::RegisterQuestion { question, fee_scale, named } })
668 }
669
670 #[must_use]
671 pub fn register_and_associate(venue: Venue, question: u32, named: Instance) -> Self {
672 OutcomeDeploy { venue, operation: Operation::RegisterAndAssociate { question, named } }
673 }
674
675 #[must_use]
680 pub fn settle_outcome(venue: Venue, settlement: Settlement) -> Self {
681 OutcomeDeploy { venue, operation: Operation::SettleOutcome(settlement) }
682 }
683
684 pub fn settle_question(
690 venue: Venue,
691 question: u32,
692 settlements: Vec<Settlement>,
693 name: String,
694 description: String,
695 ) -> Result<Self, DeployError> {
696 if settlements.is_empty() {
697 return Err(DeployError::NoSettlements);
698 }
699 if let Some(part) = settlements.iter().find(|entry| !entry.fraction.is_whole()) {
700 return Err(DeployError::NotWhole(part.outcome));
701 }
702 let winners = settlements.iter().filter(|entry| entry.fraction == Fraction::ONE).count();
703 if winners != 1 {
704 return Err(DeployError::Winners(winners));
705 }
706 let mut seen = BTreeSet::new();
707 if let Some(again) = settlements.iter().find(|entry| !seen.insert(entry.outcome)) {
708 return Err(DeployError::SettledTwice(again.outcome));
709 }
710 Ok(OutcomeDeploy { venue, operation: Operation::SettleQuestion { question, settlements, name, description } })
711 }
712
713 pub fn set_sub_deployers(venue: Venue, grants: Vec<Grant>) -> Result<Self, DeployError> {
714 if grants.is_empty() {
715 return Err(DeployError::NoGrants);
716 }
717 Ok(OutcomeDeploy { venue, operation: Operation::SetSubDeployers(grants) })
718 }
719}
720
721impl L1Action for OutcomeDeploy {
722 fn action(&self) -> Map {
723 Map::new()
724 .put("type", "outcomeDeploy")
725 .put("venue", self.venue.0.clone())
726 .put("operation", Map::new().put(self.operation.wire_name(), self.operation.body()))
727 }
728}
729
730#[derive(Debug, Clone, PartialEq, Eq)]
734pub enum ActivateOutcomeDeployer {
735 Activate(Venue),
737 Deactivate,
739}
740
741impl L1Action for ActivateOutcomeDeployer {
742 fn action(&self) -> Map {
743 let map = Map::new().put("type", "activateOutcomeDeployer");
744 match self {
745 ActivateOutcomeDeployer::Activate(venue) => {
746 map.put("activate", Map::new().put("venueName", venue.0.clone()))
747 }
748 ActivateOutcomeDeployer::Deactivate => map.put("deactivate", Value::Nil),
749 }
750 }
751}
752
753#[cfg(test)]
754#[path = "tests/actions.rs"]
755mod tests;