Skip to main content

worgl_sdk/
mint.rs

1//! What a holder knows about a mint, and what that says about each note.
2
3use serde::{Deserialize, Serialize};
4use worgl::issuer::LayerSummary;
5use worgl::{Note, Policy, PublicKey};
6
7/// A mint's rotation schedule, as its info publishes it.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Schedule {
10    /// Seconds between rotations.
11    pub period_secs: u64,
12    /// Unix time, in seconds, of the next rotation.
13    pub next_at: u64,
14}
15
16/// The parts of a mint's `GET /v1/mints/{mint}/info` answer a holder needs.
17/// Other fields in the answer are ignored.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct MintInfo {
20    /// The mint's issuer key, which every one of its notes names.
21    pub issuer: PublicKey,
22    /// Every layer, first to last.
23    pub layers: Vec<LayerSummary>,
24    /// The rules the mint enforces.
25    pub policy: Policy,
26    /// The rotation schedule, or `None` when the mint rotates only on
27    /// request.
28    #[serde(default)]
29    pub rotation: Option<Schedule>,
30    /// Whether the mint is paused. A paused mint refuses transfers and skips
31    /// its scheduled rotations.
32    #[serde(default)]
33    pub paused: bool,
34}
35
36/// A mint's info has no layers.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38#[error("the mint's info lists no layers")]
39pub struct NoLayers;
40
41/// What a holder knows about one mint: enough to tell, without asking it,
42/// whether a note can move now and when it dies.
43///
44/// A note's layer follows from its serial: generations are consecutive, so a
45/// note of generation `g` sits at position `g − oldest`. A view holds the
46/// layers as the mint's info gave them, and no clock moves it: a note counts
47/// as expired only once the info says its layer was dropped. After a
48/// rotation, read the info again.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct MintView {
51    /// The mint's issuer key.
52    pub issuer: PublicKey,
53    /// How many layers the mint has.
54    pub layers: u32,
55    /// Generation of the first layer.
56    pub oldest: u64,
57    /// The rules the mint enforces.
58    pub policy: Policy,
59    /// The rotation schedule, if the mint has one.
60    pub schedule: Option<Schedule>,
61    /// Whether the mint was paused when the view was read.
62    pub paused: bool,
63}
64
65/// Where a note stands, as far as a [`MintView`] can tell.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(tag = "state", rename_all = "snake_case")]
68pub enum NoteState {
69    /// The note can be spent now.
70    Spendable {
71        /// Untouched, the note dies in this many rotations from now; it
72        /// survives one fewer.
73        expires_after_rotations: u32,
74    },
75    /// The note sits in the last layer and cannot move until the next
76    /// rotation.
77    Frozen {
78        /// Untouched, the note dies in this many rotations from now.
79        expires_after_rotations: u32,
80    },
81    /// The note's layer was dropped, so it is worthless.
82    Expired,
83    /// The note is newer than the view: read the mint's info again.
84    Unknown,
85    /// The note belongs to another mint.
86    OtherMint,
87}
88
89impl MintView {
90    /// The view a mint's info gives.
91    pub fn new(info: &MintInfo) -> Result<Self, NoLayers> {
92        let first = info.layers.first().ok_or(NoLayers)?;
93        Ok(Self {
94            issuer: info.issuer,
95            layers: info.layers.len() as u32,
96            oldest: first.generation,
97            policy: info.policy.clone(),
98            schedule: info.rotation,
99            paused: info.paused,
100        })
101    }
102
103    /// Generation of the last layer, where transferred notes land.
104    pub fn newest(&self) -> u64 {
105        self.oldest + u64::from(self.layers) - 1
106    }
107
108    /// Position of a note's layer, 0 for the first, or `None` if the note
109    /// belongs to another mint, has expired or is newer than this view.
110    pub fn position(&self, note: &Note) -> Option<u32> {
111        let generation = note.serial().generation;
112        let mine = note.issuer() == &self.issuer;
113        (mine && generation >= self.oldest && generation <= self.newest())
114            .then(|| (generation - self.oldest) as u32)
115    }
116
117    /// Where a note stands.
118    pub fn state(&self, note: &Note) -> NoteState {
119        if note.issuer() != &self.issuer {
120            return NoteState::OtherMint;
121        }
122        let generation = note.serial().generation;
123        if generation < self.oldest {
124            return NoteState::Expired;
125        }
126        let Some(position) = self.position(note) else {
127            return NoteState::Unknown;
128        };
129        let expires_after_rotations = position + 1;
130        if position + 1 < self.layers {
131            NoteState::Spendable {
132                expires_after_rotations,
133            }
134        } else {
135            NoteState::Frozen {
136                expires_after_rotations,
137            }
138        }
139    }
140
141    /// Seconds from Unix time `now` until the next scheduled rotation, if
142    /// the mint has a schedule and is not paused. 0 once `now` reaches that
143    /// rotation, however far past: as far as the view knows, the mint has
144    /// not made it yet, so it is the one about to come.
145    pub fn until_rotation(&self, now: u64) -> Option<u64> {
146        let schedule = self.schedule.filter(|_| !self.paused)?;
147        Some(schedule.next_at.saturating_sub(now))
148    }
149
150    /// Unix time, in seconds, of the scheduled rotation that drops a note's
151    /// layer, counted from the next rotation the view names. `None` when the
152    /// mint has no schedule or is paused, or the note is not live in the
153    /// view.
154    pub fn expires_at(&self, note: &Note) -> Option<u64> {
155        let schedule = self.schedule.filter(|_| !self.paused)?;
156        let rotations = match self.state(note) {
157            NoteState::Spendable {
158                expires_after_rotations,
159            }
160            | NoteState::Frozen {
161                expires_after_rotations,
162            } => expires_after_rotations,
163            NoteState::Expired | NoteState::Unknown | NoteState::OtherMint => return None,
164        };
165        let later = u64::from(rotations - 1).saturating_mul(schedule.period_secs);
166        Some(schedule.next_at.saturating_add(later))
167    }
168}