1use 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#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Seat {
19 Free,
21 Deployer { venue: String },
23 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 #[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct OnChain {
102 pub name: String,
103 pub description: String,
104 pub sides: Option<[String; 2]>,
106}
107
108#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Unsettled<'a> {
133 Outcome(&'a Outcome),
135 Question(&'a Question),
137}
138
139impl Unsettled<'_> {
140 #[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#[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#[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#[derive(Debug, Clone, Default, PartialEq, Eq)]
192pub struct Filled {
193 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#[derive(Debug, Clone, Copy)]
213pub struct Desk<'a> {
214 pub meta: &'a OutcomeMeta,
215 pub templates: &'a [OutcomeTemplate],
217 pub seat: &'a Seat,
218 pub now_ms: u64,
220 pub limits: Option<DeployerLimits>,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
226pub enum Role {
227 Standalone,
228 Question,
229 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#[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 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#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum Action {
300 Deploy(OutcomeDeploy),
301 Activate(ActivateOutcomeDeployer),
302}
303
304impl Action {
305 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
332pub enum Back {
333 Listed { venue: String, name: String, description: String, above: u32 },
335 Settled(u32),
337 QuestionSettled(u32),
339 Granted { venue: String, grants: Vec<Grant> },
341 Activated(String),
343 Deactivated,
345}
346
347#[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#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Plan {
384 pub what: String,
386 pub action: Action,
387 pub back: Back,
388}
389
390fn 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
399fn 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
411fn 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
428fn 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
460pub 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
479pub 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 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
532pub 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
557fn settlement(row: &Outcome, fraction: Fraction) -> Result<Settlement, Refusal> {
559 row.settlement(fraction).ok_or(Refusal::NotTwoSided(row.outcome))
560}
561
562pub 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
592pub 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
632pub 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
657pub 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
675pub 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;