Skip to main content

df_hyperliquid_core/deploy/outcomes/
flow.rs

1//! A deployer's steps as pure functions: each checks typed input against a [`Desk`] and gives the exact action
2//! to sign, what it does in words and what to look for in `outcomeMeta` afterwards, or every reason it cannot.
3//! Nothing here reads or sends.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use crate::deploy::outcomes::actions::{
8    ActivateOutcomeDeployer, BadValue, DeployError, FeeScale, Fraction, Grant, Instance, Keywords, OutcomeDeploy,
9    Permission, Problem, Settlement, Venue,
10};
11use crate::markets::outcomes::meta::{Outcome, OutcomeMeta, Question};
12use crate::markets::outcomes::template::{DeployerLimits, OutcomeTemplate, TemplateRole};
13use crate::utils::Address;
14use crate::utils::sign::{L1Action, Map};
15
16/// Who the account is to a venue.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Seat {
19    /// It deploys on no venue, and may activate one.
20    Free,
21    /// It is the venue's deployer.
22    Deployer { venue: String },
23    /// Another's venue granted it some operations, by their wire names.
24    Granted { venue: String, deployer: Address, may: Vec<String> },
25}
26
27impl Seat {
28    #[must_use]
29    pub fn venue(&self) -> Option<&str> {
30        match self {
31            Seat::Free => None,
32            Seat::Deployer { venue } | Seat::Granted { venue, .. } => Some(venue),
33        }
34    }
35
36    /// Whether the account may send `operation` on its venue. A grant of
37    /// `settleQuestion` covers `settleQuestion2`.
38    #[must_use]
39    pub fn may(&self, operation: &str) -> bool {
40        match self {
41            Seat::Free => false,
42            Seat::Deployer { .. } => true,
43            Seat::Granted { may, .. } => may
44                .iter()
45                .any(|granted| granted == operation || (granted == "settleQuestion" && operation == "settleQuestion2")),
46        }
47    }
48}
49
50/// The account's seat from `outcomeMeta`'s deployers: its own venue first,
51/// then the first venue that granted it anything.
52#[must_use]
53pub fn seat_of(meta: &OutcomeMeta, account: Address) -> Seat {
54    if let Some(venue) = meta.deployers.iter().find(|one| one.deployer == account).and_then(|one| one.venue.clone()) {
55        return Seat::Deployer { venue };
56    }
57    meta.deployers
58        .iter()
59        .find_map(|one| {
60            let may: Vec<String> = one
61                .sub_deployers
62                .iter()
63                .filter(|(_, users)| users.contains(&account))
64                .map(|(operation, _)| operation.clone())
65                .collect();
66            (!may.is_empty()).then(|| Seat::Granted {
67                venue: one.venue.clone().unwrap_or_default(),
68                deployer: one.deployer,
69                may,
70            })
71        })
72        .unwrap_or(Seat::Free)
73}
74
75/// Every `outcomeMeta` row on the venue, fallbacks and expired ones included:
76/// what its cap of live outcomes counts.
77#[must_use]
78pub fn live_on(meta: &OutcomeMeta, venue: &str) -> usize {
79    meta.outcomes.iter().filter(|row| row.venue.as_deref() == Some(venue)).count()
80}
81
82/// What `address` holds on the venue now.
83#[must_use]
84pub fn held_by(meta: &OutcomeMeta, venue: &str, address: Address) -> Vec<Permission> {
85    let Some(deployer) = meta.deployer(venue) else {
86        return Vec::new();
87    };
88    Permission::ALL
89        .into_iter()
90        .filter(|permission| {
91            deployer
92                .sub_deployers
93                .iter()
94                .any(|(operation, users)| operation == permission.wire() && users.contains(&address))
95        })
96        .collect()
97}
98
99/// How the venue writes a row made from a template.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct OnChain {
102    pub name: String,
103    pub description: String,
104    /// A standalone outcome's sides; a question's outcome has `Yes` and `No`.
105    pub sides: Option<[String; 2]>,
106}
107
108/// What a row made from `template` with these values reads as in
109/// `outcomeMeta`: `template:<id>`, the keywords sorted as `key:value` with
110/// bars between, and a standalone outcome's side names under `template:`.
111#[must_use]
112pub fn on_chain(template: &OutcomeTemplate, values: &BTreeMap<String, String>) -> OnChain {
113    let mut pairs: Vec<(&str, &str)> = template
114        .keywords
115        .iter()
116        .map(|keyword| (keyword.name.as_str(), values.get(&keyword.name).map_or("", String::as_str)))
117        .collect();
118    pairs.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
119    let description = pairs.iter().map(|(key, value)| format!("{key}:{value}")).collect::<Vec<_>>().join("|");
120    let sides = match &template.role {
121        TemplateRole::Standalone { side_names } => match side_names.as_slice() {
122            [first, second] => Some([format!("template:{first}"), format!("template:{second}")]),
123            _ => None,
124        },
125        _ => None,
126    };
127    OnChain { name: format!("template:{}", template.id), description, sides }
128}
129
130/// One of the venue's rows still to settle.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Unsettled<'a> {
133    /// An outcome no question holds.
134    Outcome(&'a Outcome),
135    /// A question with a named outcome left.
136    Question(&'a Question),
137}
138
139impl Unsettled<'_> {
140    /// The outcome's id, or the question's.
141    #[must_use]
142    pub const fn id(self) -> u32 {
143        match self {
144            Unsettled::Outcome(row) => row.outcome,
145            Unsettled::Question(row) => row.question,
146        }
147    }
148}
149
150/// What on `venue` is still to settle: its outcomes outside any question,
151/// then its questions with a named outcome left, each by id.
152#[must_use]
153pub fn unsettled<'a>(meta: &'a OutcomeMeta, venue: &str) -> Vec<Unsettled<'a>> {
154    let held: BTreeSet<u32> = meta.questions.iter().flat_map(legs_of).collect();
155    let on_venue = |outcome: u32| meta.outcome(outcome).is_some_and(|row| row.venue.as_deref() == Some(venue));
156    let mut rows: Vec<Unsettled<'a>> = meta
157        .outcomes
158        .iter()
159        .filter(|row| row.venue.as_deref() == Some(venue) && !held.contains(&row.outcome))
160        .map(Unsettled::Outcome)
161        .collect();
162    rows.sort_by_key(|row| row.id());
163    let mut questions: Vec<Unsettled<'a>> =
164        questions_on_by(meta, on_venue).into_iter().map(Unsettled::Question).collect();
165    questions.sort_by_key(|row| row.id());
166    rows.extend(questions);
167    rows
168}
169
170/// The live questions on `venue`: those with a named outcome left, by id.
171#[must_use]
172pub fn questions_on<'a>(meta: &'a OutcomeMeta, venue: &str) -> Vec<&'a Question> {
173    questions_on_by(meta, |outcome| meta.outcome(outcome).is_some_and(|row| row.venue.as_deref() == Some(venue)))
174}
175
176fn questions_on_by(meta: &OutcomeMeta, on_venue: impl Fn(u32) -> bool) -> Vec<&Question> {
177    let mut found: Vec<&Question> = meta
178        .questions
179        .iter()
180        .filter(|question| legs_of(question).any(&on_venue) && question.unsettled().next().is_some())
181        .collect();
182    found.sort_by_key(|question| question.question);
183    found
184}
185
186fn legs_of(question: &Question) -> impl Iterator<Item = u32> + '_ {
187    question.named_outcomes.iter().chain(question.fallback_outcome.iter()).copied()
188}
189
190/// A template and what was typed for its keywords.
191#[derive(Debug, Clone, Default, PartialEq, Eq)]
192pub struct Filled {
193    /// The template's id in `outcomeTemplates`.
194    pub template: String,
195    pub values: BTreeMap<String, String>,
196}
197
198impl Filled {
199    #[must_use]
200    pub fn new<K: Into<String>, V: Into<String>>(
201        template: impl Into<String>,
202        values: impl IntoIterator<Item = (K, V)>,
203    ) -> Filled {
204        Filled {
205            template: template.into(),
206            values: values.into_iter().map(|(key, value)| (key.into(), value.into())).collect(),
207        }
208    }
209}
210
211/// What every step is checked against.
212#[derive(Debug, Clone, Copy)]
213pub struct Desk<'a> {
214    pub meta: &'a OutcomeMeta,
215    /// The venue's templates, as `outcomeTemplates` lists them.
216    pub templates: &'a [OutcomeTemplate],
217    pub seat: &'a Seat,
218    /// Milliseconds since 1970, for a `dateTime` keyword.
219    pub now_ms: u64,
220    /// `outcomeDeployerLimits`, when read: a register is refused past them.
221    pub limits: Option<DeployerLimits>,
222}
223
224/// The role a template is asked for.
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
226pub enum Role {
227    Standalone,
228    Question,
229    /// A named outcome of a question made from `parent`.
230    Named {
231        parent: String,
232    },
233}
234
235impl Role {
236    fn fits(&self, template: &OutcomeTemplate) -> bool {
237        match (self, &template.role) {
238            (Role::Standalone, TemplateRole::Standalone { .. }) | (Role::Question, TemplateRole::Question) => true,
239            (Role::Named { parent }, TemplateRole::QuestionOutcome { parent: of }) => parent == of,
240            _ => false,
241        }
242    }
243}
244
245/// Why a step cannot be signed.
246#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
247pub enum Refusal {
248    #[error("this account deploys on no venue; activate one first")]
249    NoVenue,
250    #[error("the venue's name: {0}")]
251    BadVenue(DeployError),
252    #[error("{venue} has not granted this account {operation}")]
253    NotGranted { venue: String, operation: &'static str },
254    #[error("only {venue}'s deployer may do this")]
255    NotDeployer { venue: String },
256    #[error("this account already deploys on {venue}")]
257    HasVenue { venue: String },
258    #[error("the venue name {0} is taken")]
259    Taken(String),
260    #[error("{venue} still lists {live} outcomes; a venue deactivates only with none")]
261    LiveOutcomes { venue: String, live: usize },
262    #[error("the venue may hold {remaining} more live outcomes and this needs {needed}")]
263    Full { needed: u32, remaining: u32 },
264    #[error("no template {id} makes {}", .role.words())]
265    NoTemplate { id: String, role: Role },
266    #[error("{}{bad}", .named.map(|at| format!("named outcome {}: ", at + 1)).unwrap_or_default())]
267    Value {
268        /// Which named outcome, counted from zero; `None` for the outcome or
269        /// question itself.
270        named: Option<usize>,
271        bad: BadValue,
272    },
273    #[error(transparent)]
274    Deploy(#[from] DeployError),
275    #[error("question {0} is no live question of this venue")]
276    NoQuestion(u32),
277    #[error("outcome {outcome} is not one this venue has left to settle")]
278    NotOpen { outcome: u32 },
279    #[error("outcome {outcome} is question {question}'s fallback, which settles with its question")]
280    Fallback { outcome: u32, question: u32 },
281    #[error("outcome {0} does not list two sides")]
282    NotTwoSided(u32),
283    #[error("nothing changes: every grant asked is already so")]
284    NothingChanges,
285}
286
287impl Role {
288    fn words(&self) -> String {
289        match self {
290            Role::Standalone => "a standalone outcome".to_owned(),
291            Role::Question => "a question".to_owned(),
292            Role::Named { parent } => format!("a named outcome of {parent}"),
293        }
294    }
295}
296
297/// What is to be signed.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum Action {
300    Deploy(OutcomeDeploy),
301    Activate(ActivateOutcomeDeployer),
302}
303
304impl Action {
305    /// The action's `type`.
306    #[must_use]
307    pub const fn request(&self) -> &'static str {
308        match self {
309            Action::Deploy(_) => "outcomeDeploy",
310            Action::Activate(_) => "activateOutcomeDeployer",
311        }
312    }
313
314    /// Only the account's own key may activate or deactivate, never an agent.
315    #[must_use]
316    pub const fn own_key_only(&self) -> bool {
317        matches!(self, Action::Activate(_))
318    }
319}
320
321impl L1Action for Action {
322    fn action(&self) -> Map {
323        match self {
324            Action::Deploy(action) => action.action(),
325            Action::Activate(action) => action.action(),
326        }
327    }
328}
329
330/// What to look for in `outcomeMeta` once the venue has taken it.
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub enum Back {
333    /// A new row on the venue with this name and description, past `above`.
334    Listed { venue: String, name: String, description: String, above: u32 },
335    /// The outcome gone from `outcomeMeta`.
336    Settled(u32),
337    /// The question's named outcomes all settled, or the question gone.
338    QuestionSettled(u32),
339    /// Each grant as asked.
340    Granted { venue: String, grants: Vec<Grant> },
341    /// The account the deployer of this venue.
342    Activated(String),
343    /// The account a deployer of no venue.
344    Deactivated,
345}
346
347/// What `outcomeMeta` shows of `back`, in words, once it does.
348#[must_use]
349pub fn found(back: &Back, meta: &OutcomeMeta, account: Address) -> Option<String> {
350    match back {
351        Back::Listed { venue, name, description, above } => meta
352            .outcomes
353            .iter()
354            .filter(|row| {
355                row.outcome > *above
356                    && row.venue.as_deref() == Some(venue.as_str())
357                    && &row.name == name
358                    && &row.description == description
359            })
360            .map(|row| row.outcome)
361            .max()
362            .map(|outcome| format!("outcome {outcome} is in outcomeMeta")),
363        Back::Settled(outcome) => (meta.outcome(*outcome).is_none())
364            .then(|| format!("outcome {outcome} is settled and gone from outcomeMeta")),
365        Back::QuestionSettled(question) => meta
366            .question(*question)
367            .is_none_or(|row| row.unsettled().next().is_none())
368            .then(|| format!("question {question} is settled")),
369        Back::Granted { venue, grants } => grants
370            .iter()
371            .all(|grant| held_by(meta, venue, grant.user).contains(&grant.permission) == grant.allowed)
372            .then(|| "outcomeMeta shows the grants as asked".to_owned()),
373        Back::Activated(venue) => (seat_of(meta, account) == Seat::Deployer { venue: venue.clone() })
374            .then(|| format!("the venue {venue} is this account's")),
375        Back::Deactivated => {
376            (!matches!(seat_of(meta, account), Seat::Deployer { .. })).then(|| "the venue is deactivated".to_owned())
377        }
378    }
379}
380
381/// A step ready to sign.
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Plan {
384    /// What it does, in words.
385    pub what: String,
386    pub action: Action,
387    pub back: Back,
388}
389
390/// The highest outcome id listed, so a new row is told from an old one.
391fn highest(meta: &OutcomeMeta) -> u32 {
392    meta.outcomes.iter().map(|row| row.outcome).max().unwrap_or(0)
393}
394
395fn plural(count: usize) -> &'static str {
396    if count == 1 { "" } else { "s" }
397}
398
399/// The seat's venue, if it may send `operation` there.
400fn venue_for(desk: &Desk<'_>, operation: &'static str) -> Result<Venue, Vec<Refusal>> {
401    let Some(name) = desk.seat.venue() else {
402        return Err(vec![Refusal::NoVenue]);
403    };
404    let venue = name.parse::<Venue>().map_err(|why| vec![Refusal::BadVenue(why)])?;
405    match desk.seat.may(operation) {
406        true => Ok(venue),
407        false => Err(vec![Refusal::NotGranted { venue: name.to_owned(), operation }]),
408    }
409}
410
411/// Room for `needed` more live outcomes, when the limits were read.
412fn room(desk: &Desk<'_>, needed: u32) -> Result<(), Refusal> {
413    match desk.limits {
414        Some(limits) if limits.active_remaining < needed => {
415            Err(Refusal::Full { needed, remaining: limits.active_remaining })
416        }
417        _ => Ok(()),
418    }
419}
420
421fn template<'a>(desk: &Desk<'a>, id: &str, role: Role) -> Result<&'a OutcomeTemplate, Refusal> {
422    desk.templates
423        .iter()
424        .find(|one| one.id == id && role.fits(one))
425        .ok_or_else(|| Refusal::NoTemplate { id: id.to_owned(), role })
426}
427
428/// The instance `filled` makes of `template`: every keyword it declares with
429/// a value its hint takes at `now_ms`, and none it does not declare.
430fn instance(
431    template: &OutcomeTemplate,
432    filled: &Filled,
433    now_ms: u64,
434    named: Option<usize>,
435) -> Result<Instance, Vec<Refusal>> {
436    let refuse = |bad: BadValue| Refusal::Value { named, bad };
437    let mut problems: Vec<Refusal> = Vec::new();
438    let mut keywords = Keywords::new();
439    for keyword in &template.keywords {
440        let value = filled.values.get(&keyword.name).map_or("", String::as_str);
441        let checked = match value.is_empty() {
442            true => Err(BadValue { keyword: keyword.name.clone(), problem: Problem::Missing }),
443            false => keyword.hint().check_at(&keyword.name, value, now_ms),
444        };
445        match checked.and_then(|()| keywords.clone().with(keyword.name.clone(), value)) {
446            Ok(more) => keywords = more,
447            Err(bad) => problems.push(refuse(bad)),
448        }
449    }
450    let declared: BTreeSet<&str> = template.keywords.iter().map(|keyword| keyword.name.as_str()).collect();
451    for name in filled.values.keys().filter(|name| !declared.contains(name.as_str())) {
452        problems.push(refuse(BadValue { keyword: name.clone(), problem: Problem::Unknown }));
453    }
454    match problems.is_empty() {
455        true => Ok(Instance::new(template.id.clone(), keywords)),
456        false => Err(problems),
457    }
458}
459
460/// `registerStandaloneOutcomeFromTemplate`: one outcome and its two sides.
461pub fn register_outcome(desk: &Desk<'_>, outcome: &Filled, fee: FeeScale) -> Result<Plan, Vec<Refusal>> {
462    let venue = venue_for(desk, Permission::RegisterStandalone.wire())?;
463    let template = template(desk, &outcome.template, Role::Standalone).map_err(|why| vec![why])?;
464    room(desk, 1).map_err(|why| vec![why])?;
465    let instance = instance(template, outcome, desk.now_ms, None)?;
466    let chain = on_chain(template, &outcome.values);
467    Ok(Plan {
468        what: format!("register a {} outcome on {venue}", template.id),
469        back: Back::Listed {
470            venue: venue.to_string(),
471            name: chain.name,
472            description: chain.description,
473            above: highest(desk.meta),
474        },
475        action: Action::Deploy(OutcomeDeploy::register_standalone(venue, instance, fee)),
476    })
477}
478
479/// `registerQuestionFromTemplate`: a question and its named outcomes, in the
480/// order given. The venue adds a fallback, so N named outcomes make N + 1.
481pub fn register_question(
482    desk: &Desk<'_>,
483    question: &Filled,
484    fee: FeeScale,
485    named: &[Filled],
486) -> Result<Plan, Vec<Refusal>> {
487    let venue = venue_for(desk, Permission::RegisterQuestion.wire())?;
488    let template = template(desk, &question.template, Role::Question).map_err(|why| vec![why])?;
489    let needed = u32::try_from(named.len()).unwrap_or(u32::MAX).saturating_add(1);
490    room(desk, needed).map_err(|why| vec![why])?;
491    let mut problems = Vec::new();
492    let asked = instance(template, question, desk.now_ms, None).map_err(|more| problems.extend(more));
493    let parent = Role::Named { parent: template.id.clone() };
494    let mut instances = Vec::with_capacity(named.len());
495    let mut first = None;
496    for (at, filled) in named.iter().enumerate() {
497        match self::template(desk, &filled.template, parent.clone()) {
498            Ok(one) => match instance(one, filled, desk.now_ms, Some(at)) {
499                Ok(made) => {
500                    first.get_or_insert_with(|| on_chain(one, &filled.values));
501                    instances.push(made);
502                }
503                Err(more) => problems.extend(more),
504            },
505            Err(why) => problems.push(why),
506        }
507    }
508    let Ok(asked) = asked else {
509        return Err(problems);
510    };
511    if !problems.is_empty() {
512        return Err(problems);
513    }
514    let count = instances.len();
515    let action =
516        OutcomeDeploy::register_question(venue.clone(), asked, fee, instances).map_err(|why| vec![why.into()])?;
517    let first = first.unwrap_or(OnChain { name: String::new(), description: String::new(), sides: None });
518    Ok(Plan {
519        what: format!("register a {} question with {count} named outcome{} on {venue}", template.id, plural(count)),
520        // The question's own row is no outcome, and its fallback is named as
521        // every fallback is, so the first named outcome is looked for.
522        back: Back::Listed {
523            venue: venue.to_string(),
524            name: first.name,
525            description: first.description,
526            above: highest(desk.meta),
527        },
528        action: Action::Deploy(action),
529    })
530}
531
532/// `registerAndAssociateNamedOutcomeFromTemplate`: one more named outcome on
533/// a live question, from a template its own template takes.
534pub fn add_to_question(desk: &Desk<'_>, question: u32, named: &Filled) -> Result<Plan, Vec<Refusal>> {
535    let venue = venue_for(desk, Permission::RegisterAndAssociate.wire())?;
536    let live = questions_on(desk.meta, venue.as_str())
537        .into_iter()
538        .find(|live| live.question == question)
539        .ok_or_else(|| vec![Refusal::NoQuestion(question)])?;
540    let parent = live.name.strip_prefix("template:").unwrap_or(&live.name).to_owned();
541    let template = template(desk, &named.template, Role::Named { parent }).map_err(|why| vec![why])?;
542    room(desk, 1).map_err(|why| vec![why])?;
543    let instance = instance(template, named, desk.now_ms, None)?;
544    let chain = on_chain(template, &named.values);
545    Ok(Plan {
546        what: format!("add a {} outcome to question {question} on {venue}", template.id),
547        back: Back::Listed {
548            venue: venue.to_string(),
549            name: chain.name,
550            description: chain.description,
551            above: highest(desk.meta),
552        },
553        action: Action::Deploy(OutcomeDeploy::register_and_associate(venue, question, instance)),
554    })
555}
556
557/// A row's settlement at `fraction`, its words copied from `outcomeMeta`.
558fn settlement(row: &Outcome, fraction: Fraction) -> Result<Settlement, Refusal> {
559    row.settlement(fraction).ok_or(Refusal::NotTwoSided(row.outcome))
560}
561
562/// `settleOutcome`, its words copied from `outcomeMeta` exactly. An outcome outside any question settles to
563/// any fraction; a question's named outcome still open settles alone only to 0 or 1 (testnet settled a
564/// question's legs one at a time as each was ruled out); a fallback settles with its question. Pays out at once
565/// and cannot be undone.
566pub fn settle_outcome(desk: &Desk<'_>, outcome: u32, fraction: Fraction) -> Result<Plan, Vec<Refusal>> {
567    let venue = venue_for(desk, Permission::SettleOutcome.wire())?;
568    let row = desk
569        .meta
570        .outcome(outcome)
571        .filter(|row| row.venue.as_deref() == Some(venue.as_str()))
572        .ok_or_else(|| vec![Refusal::NotOpen { outcome }])?;
573    if let Some(question) = desk.meta.question_of(outcome) {
574        if question.fallback_outcome == Some(outcome) {
575            return Err(vec![Refusal::Fallback { outcome, question: question.question }]);
576        }
577        if !question.unsettled().any(|leg| leg == outcome) {
578            return Err(vec![Refusal::NotOpen { outcome }]);
579        }
580        if !fraction.is_whole() {
581            return Err(vec![Refusal::Deploy(DeployError::NotWhole(outcome))]);
582        }
583    }
584    let settlement = settlement(row, fraction).map_err(|why| vec![why])?;
585    Ok(Plan {
586        what: format!("settle outcome {outcome} to {fraction} on {venue}"),
587        back: Back::Settled(outcome),
588        action: Action::Deploy(OutcomeDeploy::settle_outcome(venue, settlement)),
589    })
590}
591
592/// `settleQuestion2`: every named outcome not yet settled, `winner` at 1 and
593/// the rest at 0, the fallback left out, the question's words copied from
594/// `outcomeMeta`. Pays out at once and cannot be undone.
595pub fn settle_question(desk: &Desk<'_>, question: u32, winner: u32) -> Result<Plan, Vec<Refusal>> {
596    let venue = venue_for(desk, "settleQuestion2")?;
597    let asked = questions_on(desk.meta, venue.as_str())
598        .into_iter()
599        .find(|live| live.question == question)
600        .ok_or_else(|| vec![Refusal::NoQuestion(question)])?;
601    if !asked.unsettled().any(|leg| leg == winner) {
602        return Err(vec![Refusal::NotOpen { outcome: winner }]);
603    }
604    let mut settlements = Vec::new();
605    let mut problems = Vec::new();
606    for leg in asked.unsettled() {
607        let fraction = if leg == winner { Fraction::ONE } else { Fraction::ZERO };
608        match desk.meta.outcome(leg).ok_or(Refusal::NotOpen { outcome: leg }).and_then(|row| settlement(row, fraction))
609        {
610            Ok(made) => settlements.push(made),
611            Err(why) => problems.push(why),
612        }
613    }
614    if !problems.is_empty() {
615        return Err(problems);
616    }
617    let action = OutcomeDeploy::settle_question(
618        venue.clone(),
619        question,
620        settlements,
621        asked.name.clone(),
622        asked.description.clone(),
623    )
624    .map_err(|why| vec![why.into()])?;
625    Ok(Plan {
626        what: format!("settle question {question} with {winner} winning on {venue}"),
627        back: Back::QuestionSettled(question),
628        action: Action::Deploy(action),
629    })
630}
631
632/// `setSubDeployers`, only the deployer's: the grants asked that change what
633/// `outcomeMeta` shows, and nothing when none would.
634pub fn set_grants(desk: &Desk<'_>, grants: &[Grant]) -> Result<Plan, Vec<Refusal>> {
635    let Seat::Deployer { venue: name } = desk.seat else {
636        return Err(vec![match desk.seat.venue() {
637            Some(venue) => Refusal::NotDeployer { venue: venue.to_owned() },
638            None => Refusal::NoVenue,
639        }]);
640    };
641    let venue = name.parse::<Venue>().map_err(|why| vec![Refusal::BadVenue(why)])?;
642    let changes: Vec<Grant> = grants
643        .iter()
644        .filter(|grant| held_by(desk.meta, name, grant.user).contains(&grant.permission) != grant.allowed)
645        .copied()
646        .collect();
647    let count = changes.len();
648    let action =
649        OutcomeDeploy::set_sub_deployers(venue.clone(), changes.clone()).map_err(|_| vec![Refusal::NothingChanges])?;
650    Ok(Plan {
651        what: format!("change {count} grant{} on {venue}", plural(count)),
652        back: Back::Granted { venue: venue.to_string(), grants: changes },
653        action: Action::Deploy(action),
654    })
655}
656
657/// `activateOutcomeDeployer` for an account on no venue, under a name no one
658/// holds. The name is claimed for good. The account's own key only.
659pub fn activate(desk: &Desk<'_>, venue: Venue) -> Result<Plan, Vec<Refusal>> {
660    match desk.seat {
661        Seat::Free => {}
662        Seat::Deployer { venue: held } => return Err(vec![Refusal::HasVenue { venue: held.clone() }]),
663        Seat::Granted { venue: held, .. } => return Err(vec![Refusal::NotDeployer { venue: held.clone() }]),
664    }
665    if desk.meta.deployer(venue.as_str()).is_some() {
666        return Err(vec![Refusal::Taken(venue.to_string())]);
667    }
668    Ok(Plan {
669        what: format!("activate the venue {venue}"),
670        back: Back::Activated(venue.to_string()),
671        action: Action::Activate(ActivateOutcomeDeployer::Activate(venue)),
672    })
673}
674
675/// `activateOutcomeDeployer` deactivating the deployer's venue, which needs
676/// no live outcomes on it (and 183 days staked, which only the venue knows).
677/// It cannot be undone, and the name stays taken. The account's own key only.
678pub fn deactivate(desk: &Desk<'_>) -> Result<Plan, Vec<Refusal>> {
679    let Seat::Deployer { venue } = desk.seat else {
680        return Err(vec![match desk.seat.venue() {
681            Some(venue) => Refusal::NotDeployer { venue: venue.to_owned() },
682            None => Refusal::NoVenue,
683        }]);
684    };
685    let live = live_on(desk.meta, venue);
686    if live > 0 {
687        return Err(vec![Refusal::LiveOutcomes { venue: venue.clone(), live }]);
688    }
689    Ok(Plan {
690        what: format!("deactivate the venue {venue}"),
691        back: Back::Deactivated,
692        action: Action::Activate(ActivateOutcomeDeployer::Deactivate),
693    })
694}
695
696#[cfg(test)]
697#[path = "tests/flow.rs"]
698mod tests;