use std::{
collections::{BTreeMap, BTreeSet, btree_map::Entry},
fmt,
str::FromStr,
};
use crate::utils::sign::{L1Action, Map, Value};
use crate::utils::time::Stamp;
use crate::utils::{Address, Dec};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DeployError {
#[error("`{0}` is no venue name: a venue is 2 to 4 lower case letters")]
Venue(String),
#[error("`spot` is the spot venue's name and cannot be claimed")]
SpotVenue,
#[error("a fee scale is a decimal from 0 to 10, which `{0}` is not")]
FeeScale(String),
#[error("a settle fraction is a decimal from 0 to 1, which `{0}` is not")]
Fraction(String),
#[error("a question needs at least one named outcome")]
NoNamedOutcomes,
#[error("setting sub-deployers needs at least one grant")]
NoGrants,
#[error("settling a question needs each of its named outcomes not yet settled")]
NoSettlements,
#[error("outcome {0} belongs to a question, which settles to 0 or 1 only")]
NotWhole(u32),
#[error("a question settles with exactly one named outcome at 1, not {0}")]
Winners(usize),
#[error("outcome {0} is settled twice")]
SettledTwice(u32),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Venue(String);
impl Venue {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Venue {
type Err = DeployError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
let letters = name.bytes().all(|byte| byte.is_ascii_lowercase());
if !(2..=4).contains(&name.len()) || !letters {
return Err(DeployError::Venue(name.to_owned()));
}
if name == "spot" {
return Err(DeployError::SpotVenue);
}
Ok(Venue(name.to_owned()))
}
}
impl fmt::Display for Venue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn bounded(text: &str, most: Dec) -> Option<Dec> {
let text = text.trim();
if text.starts_with(['+', '-']) {
return None;
}
let value = Dec::parse(text).ok()?;
(!value.is_negative() && value <= most).then_some(value)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FeeScale(Dec);
impl FeeScale {
pub const ZERO: FeeScale = FeeScale(Dec::ZERO);
pub const ONE: FeeScale = FeeScale(Dec::ONE);
#[must_use]
pub const fn value(self) -> Dec {
self.0
}
#[must_use]
pub fn to_wire(self) -> String {
self.0.to_wire()
}
}
impl FromStr for FeeScale {
type Err = DeployError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
bounded(text, Dec::from_int(10)).map(FeeScale).ok_or_else(|| DeployError::FeeScale(text.trim().to_owned()))
}
}
impl fmt::Display for FeeScale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fraction(Dec);
impl Fraction {
pub const ZERO: Fraction = Fraction(Dec::ZERO);
pub const ONE: Fraction = Fraction(Dec::ONE);
#[must_use]
pub const fn value(self) -> Dec {
self.0
}
#[must_use]
pub fn to_wire(self) -> String {
self.0.to_wire()
}
#[must_use]
pub fn is_whole(self) -> bool {
self == Fraction::ZERO || self == Fraction::ONE
}
}
impl FromStr for Fraction {
type Err = DeployError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
bounded(text, Dec::ONE).map(Fraction).ok_or_else(|| DeployError::Fraction(text.trim().to_owned()))
}
}
impl fmt::Display for Fraction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
pub const MAX_VALUE_LEN: usize = 100;
pub const MAX_SHORT_LEN: usize = 10;
pub const HORIZON_MS: u64 = 366 * 24 * 60 * 60 * 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Problem {
Missing,
TooLong,
Reserved,
DateTime,
Date,
ShortString,
UInt,
TooLarge,
UDecimal,
Passed,
TooFar,
Unknown,
Repeated,
}
impl Problem {
#[must_use]
pub const fn words(self) -> &'static str {
match self {
Problem::Missing => "needs a value",
Problem::TooLong => "takes at most 100 characters",
Problem::Reserved => "cannot hold {, } or |",
Problem::DateTime => "takes YYYYMMDD-HHMM in UTC",
Problem::Date => "takes YYYYMMDD in UTC",
Problem::ShortString => "takes at most 10 characters",
Problem::UInt => "takes a whole number with no sign or leading zeros",
Problem::TooLarge => "takes a whole number that fits in 64 bits",
Problem::UDecimal => "takes a decimal with no sign, no exponent and no leading or trailing zeros",
Problem::Passed => "has already passed",
Problem::TooFar => "is more than a year away",
Problem::Unknown => "is not a keyword of this template",
Problem::Repeated => "is given twice",
}
}
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.words())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{keyword} {problem}")]
pub struct BadValue {
pub keyword: String,
pub problem: Problem,
}
impl BadValue {
fn new(keyword: &str, problem: Problem) -> Self {
BadValue { keyword: keyword.to_owned(), problem }
}
}
fn plain(keyword: &str, value: &str) -> Result<(), BadValue> {
let problem = if value.is_empty() {
Problem::Missing
} else if value.len() > MAX_VALUE_LEN {
Problem::TooLong
} else if value.contains(['{', '}', '|']) {
Problem::Reserved
} else {
return Ok(());
};
Err(BadValue::new(keyword, problem))
}
fn is_uint(text: &str) -> bool {
let digits = !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
digits && (text == "0" || !text.starts_with('0'))
}
fn is_udecimal(text: &str) -> bool {
let (whole, fraction) = match text.split_once('.') {
Some((whole, fraction)) => (whole, Some(fraction)),
None => (text, None),
};
is_uint(whole)
&& fraction.is_none_or(|fraction| {
!fraction.is_empty() && fraction.bytes().all(|byte| byte.is_ascii_digit()) && !fraction.ends_with('0')
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Hint {
DateTime,
Date,
ShortString,
HlPerp,
UInt,
UDecimal,
String,
Other(String),
}
impl Hint {
#[must_use]
pub fn parse(name: &str) -> Hint {
match name {
"dateTime" => Hint::DateTime,
"date" => Hint::Date,
"shortString" => Hint::ShortString,
"hlPerp" => Hint::HlPerp,
"uInt" => Hint::UInt,
"uDecimal" => Hint::UDecimal,
"string" => Hint::String,
other => Hint::Other(other.to_owned()),
}
}
#[must_use]
pub fn name(&self) -> &str {
match self {
Hint::DateTime => "dateTime",
Hint::Date => "date",
Hint::ShortString => "shortString",
Hint::HlPerp => "hlPerp",
Hint::UInt => "uInt",
Hint::UDecimal => "uDecimal",
Hint::String => "string",
Hint::Other(name) => name,
}
}
pub fn check(&self, keyword: &str, value: &str) -> Result<(), BadValue> {
plain(keyword, value)?;
let problem = match self {
Hint::DateTime if Stamp::exact(value).is_none() => Problem::DateTime,
Hint::Date if Stamp::day(value).is_none() => Problem::Date,
Hint::ShortString if value.len() > MAX_SHORT_LEN => Problem::ShortString,
Hint::UInt if !is_uint(value) => Problem::UInt,
Hint::UInt if value.parse::<u64>().is_err() => Problem::TooLarge,
Hint::UDecimal if !is_udecimal(value) => Problem::UDecimal,
_ => return Ok(()),
};
Err(BadValue::new(keyword, problem))
}
pub fn check_at(&self, keyword: &str, value: &str, now_ms: u64) -> Result<(), BadValue> {
self.check(keyword, value)?;
if *self != Hint::DateTime {
return Ok(());
}
let Some(stamp) = Stamp::exact(value) else {
return Err(BadValue::new(keyword, Problem::DateTime));
};
let at = i128::from(stamp.ms());
let now = i128::from(now_ms);
let problem = if at <= now {
Problem::Passed
} else if at > now + i128::from(HORIZON_MS) {
Problem::TooFar
} else {
return Ok(());
};
Err(BadValue::new(keyword, problem))
}
}
impl fmt::Display for Hint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Keywords(BTreeMap<String, String>);
impl Keywords {
#[must_use]
pub const fn new() -> Self {
Keywords(BTreeMap::new())
}
pub fn from_pairs<K: Into<String>, V: Into<String>>(
pairs: impl IntoIterator<Item = (K, V)>,
) -> Result<Self, BadValue> {
pairs.into_iter().try_fold(Keywords::new(), |keywords, (keyword, value)| keywords.with(keyword, value))
}
pub fn with(mut self, keyword: impl Into<String>, value: impl Into<String>) -> Result<Self, BadValue> {
let keyword = keyword.into();
let value = value.into();
plain(&keyword, &value)?;
match self.0.entry(keyword) {
Entry::Occupied(taken) => Err(BadValue::new(taken.key(), Problem::Repeated)),
Entry::Vacant(free) => {
free.insert(value);
Ok(self)
}
}
}
#[must_use]
pub fn get(&self, keyword: &str) -> Option<&str> {
self.0.get(keyword).map(String::as_str)
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0.iter().map(|(keyword, value)| (keyword.as_str(), value.as_str()))
}
#[must_use]
pub fn wire(&self) -> Value {
Value::array(self.iter().map(|(keyword, value)| Value::array([keyword.to_owned(), value.to_owned()])))
}
#[must_use]
pub fn check<'a>(&self, declared: impl IntoIterator<Item = (&'a str, Hint)>, now_ms: u64) -> Vec<BadValue> {
let declared: BTreeMap<&str, Hint> = declared.into_iter().collect();
let mut problems = Vec::new();
for (keyword, hint) in &declared {
match self.get(keyword) {
None => problems.push(BadValue::new(keyword, Problem::Missing)),
Some(value) => problems.extend(hint.check_at(keyword, value, now_ms).err()),
}
}
for keyword in self.0.keys() {
if !declared.contains_key(keyword.as_str()) {
problems.push(BadValue::new(keyword, Problem::Unknown));
}
}
problems
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Instance {
pub id: String,
pub keywords: Keywords,
}
impl Instance {
#[must_use]
pub fn new(id: impl Into<String>, keywords: Keywords) -> Self {
Instance { id: id.into(), keywords }
}
#[must_use]
pub fn wire(&self) -> Map {
Map::new().put("id", self.id.clone()).put("keywordToValue", self.keywords.wire())
}
fn wire_with(&self, fee_scale: FeeScale) -> Map {
self.wire().put("deployerFeeScale", fee_scale.to_wire())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Settlement {
pub outcome: u32,
pub fraction: Fraction,
pub name: String,
pub description: String,
pub sides: [String; 2],
}
impl Settlement {
#[must_use]
pub fn wire(&self) -> Map {
let [first, second] = &self.sides;
Map::new()
.put("outcome", self.outcome)
.put("settleFraction", self.fraction.to_wire())
.put("details", "")
.put("nameAndDescription", Value::array([self.name.clone(), self.description.clone()]))
.put("sideNames", Value::array([first.clone(), second.clone()]))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Permission {
RegisterStandalone,
RegisterQuestion,
RegisterAndAssociate,
SettleOutcome,
SettleQuestion,
}
impl Permission {
pub const ALL: [Permission; 5] = [
Permission::RegisterStandalone,
Permission::RegisterQuestion,
Permission::RegisterAndAssociate,
Permission::SettleOutcome,
Permission::SettleQuestion,
];
#[must_use]
pub const fn wire(self) -> &'static str {
match self {
Permission::RegisterStandalone => "registerStandaloneOutcomeFromTemplate",
Permission::RegisterQuestion => "registerQuestionFromTemplate",
Permission::RegisterAndAssociate => "registerAndAssociateNamedOutcomeFromTemplate",
Permission::SettleOutcome => "settleOutcome",
Permission::SettleQuestion => "settleQuestion",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Grant {
pub permission: Permission,
pub user: Address,
pub allowed: bool,
}
impl Grant {
#[must_use]
pub fn wire(&self) -> Map {
Map::new().put("variant", self.permission.wire()).put("user", self.user.to_hex()).put("allowed", self.allowed)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operation {
RegisterStandalone { instance: Instance, fee_scale: FeeScale },
RegisterQuestion { question: Instance, fee_scale: FeeScale, named: Vec<Instance> },
RegisterAndAssociate { question: u32, named: Instance },
SettleOutcome(Settlement),
SettleQuestion { question: u32, settlements: Vec<Settlement>, name: String, description: String },
SetSubDeployers(Vec<Grant>),
}
impl Operation {
#[must_use]
pub const fn wire_name(&self) -> &'static str {
match self {
Operation::RegisterStandalone { .. } => "registerStandaloneOutcomeFromTemplate",
Operation::RegisterQuestion { .. } => "registerQuestionFromTemplate",
Operation::RegisterAndAssociate { .. } => "registerAndAssociateNamedOutcomeFromTemplate",
Operation::SettleOutcome(_) => "settleOutcome",
Operation::SettleQuestion { .. } => "settleQuestion2",
Operation::SetSubDeployers(_) => "setSubDeployers",
}
}
#[must_use]
pub const fn permission(&self) -> Option<Permission> {
match self {
Operation::RegisterStandalone { .. } => Some(Permission::RegisterStandalone),
Operation::RegisterQuestion { .. } => Some(Permission::RegisterQuestion),
Operation::RegisterAndAssociate { .. } => Some(Permission::RegisterAndAssociate),
Operation::SettleOutcome(_) => Some(Permission::SettleOutcome),
Operation::SettleQuestion { .. } => Some(Permission::SettleQuestion),
Operation::SetSubDeployers(_) => None,
}
}
#[must_use]
pub const fn is_settlement(&self) -> bool {
matches!(self, Operation::SettleOutcome(_) | Operation::SettleQuestion { .. })
}
#[must_use]
pub fn body(&self) -> Value {
match self {
Operation::RegisterStandalone { instance, fee_scale } => instance.wire_with(*fee_scale).into(),
Operation::RegisterQuestion { question, fee_scale, named } => Map::new()
.put("questionTemplateInstance", question.wire_with(*fee_scale))
.put("namedOutcomeTemplateInstances", Value::array(named.iter().map(Instance::wire)))
.into(),
Operation::RegisterAndAssociate { question, named } => {
Map::new().put("question", *question).put("namedOutcomeTemplateInstance", named.wire()).into()
}
Operation::SettleOutcome(settlement) => settlement.wire().into(),
Operation::SettleQuestion { question, settlements, name, description } => Map::new()
.put("question", *question)
.put("outcomeSettlements", Value::array(settlements.iter().map(Settlement::wire)))
.put("nameAndDescription", Value::array([name.clone(), description.clone()]))
.into(),
Operation::SetSubDeployers(grants) => Value::array(grants.iter().map(Grant::wire)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutcomeDeploy {
pub venue: Venue,
pub operation: Operation,
}
impl OutcomeDeploy {
#[must_use]
pub fn register_standalone(venue: Venue, instance: Instance, fee_scale: FeeScale) -> Self {
OutcomeDeploy { venue, operation: Operation::RegisterStandalone { instance, fee_scale } }
}
pub fn register_question(
venue: Venue,
question: Instance,
fee_scale: FeeScale,
named: Vec<Instance>,
) -> Result<Self, DeployError> {
if named.is_empty() {
return Err(DeployError::NoNamedOutcomes);
}
Ok(OutcomeDeploy { venue, operation: Operation::RegisterQuestion { question, fee_scale, named } })
}
#[must_use]
pub fn register_and_associate(venue: Venue, question: u32, named: Instance) -> Self {
OutcomeDeploy { venue, operation: Operation::RegisterAndAssociate { question, named } }
}
#[must_use]
pub fn settle_outcome(venue: Venue, settlement: Settlement) -> Self {
OutcomeDeploy { venue, operation: Operation::SettleOutcome(settlement) }
}
pub fn settle_question(
venue: Venue,
question: u32,
settlements: Vec<Settlement>,
name: String,
description: String,
) -> Result<Self, DeployError> {
if settlements.is_empty() {
return Err(DeployError::NoSettlements);
}
if let Some(part) = settlements.iter().find(|entry| !entry.fraction.is_whole()) {
return Err(DeployError::NotWhole(part.outcome));
}
let winners = settlements.iter().filter(|entry| entry.fraction == Fraction::ONE).count();
if winners != 1 {
return Err(DeployError::Winners(winners));
}
let mut seen = BTreeSet::new();
if let Some(again) = settlements.iter().find(|entry| !seen.insert(entry.outcome)) {
return Err(DeployError::SettledTwice(again.outcome));
}
Ok(OutcomeDeploy { venue, operation: Operation::SettleQuestion { question, settlements, name, description } })
}
pub fn set_sub_deployers(venue: Venue, grants: Vec<Grant>) -> Result<Self, DeployError> {
if grants.is_empty() {
return Err(DeployError::NoGrants);
}
Ok(OutcomeDeploy { venue, operation: Operation::SetSubDeployers(grants) })
}
}
impl L1Action for OutcomeDeploy {
fn action(&self) -> Map {
Map::new()
.put("type", "outcomeDeploy")
.put("venue", self.venue.0.clone())
.put("operation", Map::new().put(self.operation.wire_name(), self.operation.body()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActivateOutcomeDeployer {
Activate(Venue),
Deactivate,
}
impl L1Action for ActivateOutcomeDeployer {
fn action(&self) -> Map {
let map = Map::new().put("type", "activateOutcomeDeployer");
match self {
ActivateOutcomeDeployer::Activate(venue) => {
map.put("activate", Map::new().put("venueName", venue.0.clone()))
}
ActivateOutcomeDeployer::Deactivate => map.put("deactivate", Value::Nil),
}
}
}
#[cfg(test)]
#[path = "tests/actions.rs"]
mod tests;