Skip to main content

df_hyperliquid_core/deploy/outcomes/
actions.rs

1//! What an outcome deployer signs, all L1 actions: claiming a venue, registering outcomes and questions from
2//! templates, settling them, and letting other addresses do either. Values are checked when made, not when
3//! signed. **Settling pays out at once and cannot be undone.**
4
5use 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/// Why an action could not be made: what the venue would refuse, or what
16/// would pay out the wrong way, before anything is signed.
17#[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/// A venue name: 2 to 4 lower case letters, never `spot`. Activating claims
42/// it for good; it stays taken after deactivating. Whether a perp dex holds
43/// the name already is for the caller to ask `perpDexs`.
44#[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    /// Refuses rather than lowercases: `AB` is not taken to mean `ab`.
58    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
76/// `text` as a decimal from 0 to `most`, written with no sign and no
77/// exponent. It goes on the wire in normal form: `1.0` as `1`.
78fn 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/// What a deployer takes of the fees on its outcomes, from 0 to 10.
88/// `outcomeMeta` reads a scale back as `1.0`; it is sent as `1`. Only a
89/// standalone outcome and a question carry one: a named outcome added later
90/// takes its question's.
91#[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    /// The string hashed: no trailing zeros.
104    #[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    /// Takes `1.0`, `01` or `.50`, and keeps what the venue hashes: `1`, `1`,
114    /// `0.5`.
115    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/// The share of an outcome's pool its first side is paid when it settles,
127/// from 0 to 1: `1` pays the first side in full and `0` the second. A
128/// standalone outcome settles to any fraction; a question's only to 0 or 1.
129#[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    /// Exactly 0 or exactly 1, the only fractions a question's outcome takes.
147    #[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    /// As [`FeeScale`]'s: `1.0` is kept as `1` and `.50` as `0.5`.
157    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
168/// The longest value a keyword takes, in bytes; whether the venue counts bytes or characters is not confirmed.
169pub const MAX_VALUE_LEN: usize = 100;
170
171/// The longest value a `shortString` keyword takes, counted the same way.
172pub const MAX_SHORT_LEN: usize = 10;
173
174/// How far ahead a `dateTime` value may point: 366 days, in milliseconds.
175pub const HORIZON_MS: u64 = 366 * 24 * 60 * 60 * 1000;
176
177/// What is wrong with one keyword's value, in words for the person typing it.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum Problem {
180    Missing,
181    TooLong,
182    /// `{`, `}` or `|`, which would break the `keyword:value|...` description
183    /// the venue writes from the values.
184    Reserved,
185    DateTime,
186    Date,
187    ShortString,
188    UInt,
189    /// A `uInt` of 2^64 or more.
190    TooLarge,
191    UDecimal,
192    /// A `dateTime` at or before now.
193    Passed,
194    /// A `dateTime` more than [`HORIZON_MS`] after now.
195    TooFar,
196    /// A value for a keyword the template does not declare.
197    Unknown,
198    Repeated,
199}
200
201impl Problem {
202    /// Said after the keyword's name: `time takes YYYYMMDD-HHMM in UTC`.
203    #[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/// A keyword's value the venue would refuse, and why.
230#[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
243/// What every value must be, whatever its hint: present, at most
244/// [`MAX_VALUE_LEN`] bytes, and free of `{`, `}` and `|`.
245fn 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
258/// `0`, or digits that do not start with a zero.
259fn 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
264/// A `uInt`, then optionally a point and digits that do not end in a zero.
265fn 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/// How a template says a keyword's value is written, by the name
277/// `outcomeTemplates` gives next to each keyword.
278#[derive(Debug, Clone, PartialEq, Eq, Hash)]
279pub enum Hint {
280    /// `YYYYMMDD-HHMM` in UTC, after now and within a year of it.
281    DateTime,
282    /// `YYYYMMDD`.
283    Date,
284    /// At most [`MAX_SHORT_LEN`] bytes.
285    ShortString,
286    /// A perp's name. Whether one by that name trades is for the caller to
287    /// check against the live universe; here it only has to be present.
288    HlPerp,
289    /// `0`, or digits with no leading zero, below 2^64.
290    UInt,
291    /// A `uInt`, then optionally a point and digits not ending in a zero.
292    UDecimal,
293    /// Free text.
294    String,
295    /// A hint this build does not know, checked as free text and kept under
296    /// its own name.
297    Other(String),
298}
299
300impl Hint {
301    /// Never fails: a name this build does not know becomes [`Hint::Other`].
302    #[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    /// As `outcomeTemplates` spells it.
317    #[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    /// Every rule for `value` that needs no clock. A `dateTime` is only
332    /// checked for its shape here; [`Hint::check_at`] adds when it may be.
333    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    /// [`Hint::check`], and a `dateTime` after `now_ms` and at most
348    /// [`HORIZON_MS`] after it, as the venue asks. A `date` has no rule on
349    /// when it may be.
350    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/// A template instance's `keywordToValue`: one value per keyword, kept in the
378/// order the venue hashes them, which is bytewise by keyword, so `Zone` comes
379/// before `apple`. Built sorted, so no caller can send them otherwise.
380#[derive(Debug, Clone, PartialEq, Eq, Default)]
381pub struct Keywords(BTreeMap<String, String>);
382
383impl Keywords {
384    /// No keywords, which a template that declares none is instanced with.
385    #[must_use]
386    pub const fn new() -> Self {
387        Keywords(BTreeMap::new())
388    }
389
390    /// Pairs in any order. A keyword given twice is refused, not replaced.
391    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    /// One more pair. A keyword given twice is refused, not replaced.
398    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    /// The pairs in the order they are hashed in.
427    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    /// `[[keyword, value], ...]`, or `[]` for none.
432    #[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    /// Everything wrong with these as an instance of a template declaring
438    /// `declared`, each keyword with its hint: a declared keyword with no
439    /// value, a value for a keyword it does not declare, and each value its
440    /// hint refuses at `now_ms`. Empty when nothing is.
441    #[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/// A template and a value for each of its keywords: `{"id","keywordToValue"}`.
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub struct Instance {
463    /// The template's id in `outcomeTemplates`, such as `binaryPrice`.
464    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    /// With the fee scale a standalone outcome or a question carries, last.
480    fn wire_with(&self, fee_scale: FeeScale) -> Map {
481        self.wire().put("deployerFeeScale", fee_scale.to_wire())
482    }
483}
484
485/// One outcome settled: the body of `settleOutcome`, and each entry of
486/// `settleQuestion2`. The name, description and side names must be the ones
487/// `outcomeMeta` lists, character for character, trailing spaces included
488/// ([`crate::markets::outcomes::Outcome::settlement`] copies them); they are sent as given.
489#[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    /// The first side is the one `fraction` pays.
496    pub sides: [String; 2],
497}
498
499impl Settlement {
500    /// `details` is always empty: the venue refuses anything else.
501    #[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/// What a sub-deployer may do for a venue: a `setSubDeployers` grant's
514/// `variant`.
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum Permission {
517    RegisterStandalone,
518    RegisterQuestion,
519    RegisterAndAssociate,
520    SettleOutcome,
521    /// Covers `settleQuestion2`, under the older name.
522    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/// One address let in to, or shut out of, one [`Permission`] on the venue.
547#[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/// What an `outcomeDeploy` does, under the key naming it.
562#[derive(Debug, Clone, PartialEq, Eq)]
563pub enum Operation {
564    /// `registerStandaloneOutcomeFromTemplate`: one outcome with two sides.
565    RegisterStandalone { instance: Instance, fee_scale: FeeScale },
566    /// `registerQuestionFromTemplate`: a question and its named outcomes, in
567    /// the order given, which is the order they are shown in. The venue adds
568    /// a fallback, so N named outcomes make N + 1.
569    RegisterQuestion { question: Instance, fee_scale: FeeScale, named: Vec<Instance> },
570    /// `registerAndAssociateNamedOutcomeFromTemplate`: one more named outcome
571    /// on a live question, at the question's fee scale.
572    RegisterAndAssociate { question: u32, named: Instance },
573    /// `settleOutcome`. Pays out at once and cannot be undone.
574    SettleOutcome(Settlement),
575    /// `settleQuestion2`: every named outcome of the question not yet
576    /// settled, exactly one of them at 1, the fallback left out. Pays out at
577    /// once and cannot be undone.
578    SettleQuestion { question: u32, settlements: Vec<Settlement>, name: String, description: String },
579    /// `setSubDeployers`.
580    SetSubDeployers(Vec<Grant>),
581}
582
583impl Operation {
584    /// The key the operation travels under.
585    #[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    /// The grant a sub-deployer needs to send this; `None` for
598    /// `setSubDeployers`, which no grant covers.
599    #[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    /// Settling pays out at once and cannot be undone.
612    #[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/// `{"type":"outcomeDeploy","venue","operation":{<name>: ...}}`: every register
640/// and settle, sent by the venue's deployer or an address it granted the
641/// operation's [`Permission`].
642///
643/// The constructors check what [`Operation`] alone cannot: that a question
644/// has a named outcome, that a grant list is not empty, and that a question
645/// settles with one winner. Settling pays out at once and cannot be undone.
646#[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    /// Any fraction for a standalone outcome. A question's outcome takes only
676    /// 0 or 1, which this cannot tell, so the caller keeps to that;
677    /// [`crate::deploy::outcomes::flow::settle_outcome`] tells from `outcomeMeta`. Pays out at
678    /// once and cannot be undone.
679    #[must_use]
680    pub fn settle_outcome(venue: Venue, settlement: Settlement) -> Self {
681        OutcomeDeploy { venue, operation: Operation::SettleOutcome(settlement) }
682    }
683
684    /// `settlements` in the order given, each at 0 or 1, exactly one at 1, no
685    /// outcome twice. That they are every named outcome not yet settled, and
686    /// that `name` and `description` are the question's own, is for the
687    /// caller to take from `outcomeMeta`. Pays out at once and cannot be
688    /// undone.
689    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/// `activateOutcomeDeployer`, which only the account's own key may sign, never an agent. Activating claims the
731/// venue name for good and stakes HYPE for at least 183 days; deactivating needs those days behind it and no
732/// live outcomes, and cannot be undone.
733#[derive(Debug, Clone, PartialEq, Eq)]
734pub enum ActivateOutcomeDeployer {
735    /// `{"activate":{"venueName"}}`.
736    Activate(Venue),
737    /// `{"deactivate":null}`: the key is sent, with nil.
738    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;