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}