Skip to main content

mina_archive_sdk/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// Filter for the consensus status of a block.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6pub enum BlockStatusFilter {
7    All,
8    Pending,
9    Canonical,
10}
11
12/// Sort direction for block queries.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum BlockSortBy {
15    #[serde(rename = "BLOCKHEIGHT_ASC")]
16    Asc,
17    #[serde(rename = "BLOCKHEIGHT_DESC")]
18    Desc,
19}
20
21/// Filter events from a specific account.
22///
23/// Only `address` is required. Omitting `token_id` makes the server default to
24/// the MINA token, `wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf`, and
25/// omitting `status` defaults it to `BlockStatusFilter::All`.
26///
27/// **A range-less query is not "everything".** The server caps the block scan
28/// at its configured `BLOCK_RANGE_SIZE` (10,000 by default), and the SDL warns
29/// that you can get a *partial result* if you do not specify both `from` and
30/// `to`. Page through a wide history with explicit bounds.
31///
32/// GraphQL `Int` is signed **32-bit**, so the block-height bounds are `i32`.
33/// A height computed from a `u64` now fails to compile instead of coming back
34/// as a runtime GraphQL validation error. Output heights stay `i64` — widening
35/// an output is safe and the server already sends values as `Int`.
36#[derive(Debug, Clone, Default, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct EventFilterOptionsInput {
39    pub address: String,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub token_id: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub status: Option<BlockStatusFilter>,
44    /// Inclusive lower block-height bound. `Int` is 32-bit — see the type doc.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub from: Option<i32>,
47    /// Exclusive upper block-height bound. `Int` is 32-bit — see the type doc.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub to: Option<i32>,
50}
51
52impl EventFilterOptionsInput {
53    /// Start building a filter with the required `address`.
54    pub fn for_address(address: impl Into<String>) -> Self {
55        Self {
56            address: address.into(),
57            ..Default::default()
58        }
59    }
60
61    pub fn token_id(mut self, token_id: impl Into<String>) -> Self {
62        self.token_id = Some(token_id.into());
63        self
64    }
65    pub fn status(mut self, status: BlockStatusFilter) -> Self {
66        self.status = Some(status);
67        self
68    }
69    /// Filter from this block height, **inclusive**.
70    pub fn from(mut self, from: i32) -> Self {
71        self.from = Some(from);
72        self
73    }
74    /// Filter to this block height, **exclusive**.
75    pub fn to(mut self, to: i32) -> Self {
76        self.to = Some(to);
77        self
78    }
79}
80
81/// Filter actions from a specific account.
82///
83/// Only `address` is required. Omitting `token_id` makes the server default to
84/// the MINA token, `wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf`, and
85/// omitting `status` defaults it to `BlockStatusFilter::All`.
86///
87/// **A range-less query is not "everything".** The server caps the block scan
88/// at its configured `BLOCK_RANGE_SIZE` (10,000 by default), and the SDL warns
89/// that you can get a *partial result* if you do not specify both `from` and
90/// `to`. Page through a wide history with explicit bounds.
91///
92/// GraphQL `Int` is signed **32-bit**, so the block-height bounds are `i32`.
93/// A height computed from a `u64` now fails to compile instead of coming back
94/// as a runtime GraphQL validation error. Output heights stay `i64` — widening
95/// an output is safe and the server already sends values as `Int`.
96#[derive(Debug, Clone, Default, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct ActionFilterOptionsInput {
99    pub address: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub token_id: Option<String>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub status: Option<BlockStatusFilter>,
104    /// Inclusive lower block-height bound. `Int` is 32-bit — see the type doc.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub from: Option<i32>,
107    /// Exclusive upper block-height bound. `Int` is 32-bit — see the type doc.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub to: Option<i32>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub from_action_state: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub end_action_state: Option<String>,
114}
115
116impl ActionFilterOptionsInput {
117    pub fn for_address(address: impl Into<String>) -> Self {
118        Self {
119            address: address.into(),
120            ..Default::default()
121        }
122    }
123    pub fn token_id(mut self, v: impl Into<String>) -> Self {
124        self.token_id = Some(v.into());
125        self
126    }
127    pub fn status(mut self, status: BlockStatusFilter) -> Self {
128        self.status = Some(status);
129        self
130    }
131    /// Filter from this block height, **inclusive**.
132    pub fn from(mut self, from: i32) -> Self {
133        self.from = Some(from);
134        self
135    }
136    /// Filter to this block height, **exclusive**.
137    pub fn to(mut self, to: i32) -> Self {
138        self.to = Some(to);
139        self
140    }
141    pub fn from_action_state(mut self, v: impl Into<String>) -> Self {
142        self.from_action_state = Some(v.into());
143        self
144    }
145    pub fn end_action_state(mut self, v: impl Into<String>) -> Self {
146        self.end_action_state = Some(v.into());
147        self
148    }
149}
150
151/// Filter for `verificationKeyUpdates`.
152///
153/// The block range is required, unlike the event and action filters: the
154/// server bounds the span by its configured `BLOCK_RANGE_SIZE`. `from` is
155/// inclusive and `to` is exclusive.
156///
157/// GraphQL `Int` is signed **32-bit**, so the block-height bounds are `i32`.
158/// A height computed from a `u64` now fails to compile instead of coming back
159/// as a runtime GraphQL validation error. Output heights stay `i64` — widening
160/// an output is safe and the server already sends values as `Int`.
161#[derive(Debug, Clone, Serialize)]
162#[serde(rename_all = "camelCase")]
163pub struct VerificationKeyUpdateFilterInput {
164    pub verification_key_hash: String,
165    /// Inclusive lower block-height bound. `Int` is 32-bit — see the type doc.
166    pub from: i32,
167    /// Exclusive upper block-height bound. `Int` is 32-bit — see the type doc.
168    pub to: i32,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub status: Option<BlockStatusFilter>,
171}
172
173impl VerificationKeyUpdateFilterInput {
174    /// Search `[from, to)` for account updates that set `verification_key_hash`.
175    pub fn new(verification_key_hash: impl Into<String>, from: i32, to: i32) -> Self {
176        Self {
177            verification_key_hash: verification_key_hash.into(),
178            from,
179            to,
180            status: None,
181        }
182    }
183
184    pub fn status(mut self, v: BlockStatusFilter) -> Self {
185        self.status = Some(v);
186        self
187    }
188}
189
190/// Filter blocks by height, date, or canonical status.
191///
192/// GraphQL `Int` is signed **32-bit**, so the block-height bounds are `i32`.
193/// A height computed from a `u64` now fails to compile instead of coming back
194/// as a runtime GraphQL validation error. Output heights stay `i64` — widening
195/// an output is safe and the server already sends values as `Int`.
196#[derive(Debug, Clone, Default, Serialize)]
197pub struct BlockQueryInput {
198    #[serde(rename = "blockHeight_gte", skip_serializing_if = "Option::is_none")]
199    pub block_height_gte: Option<i32>,
200    #[serde(rename = "blockHeight_lt", skip_serializing_if = "Option::is_none")]
201    pub block_height_lt: Option<i32>,
202    /// Inclusive lower bound, as an ISO-8601 / RFC 3339 instant.
203    ///
204    /// The server coerces this with JavaScript's `new Date(value).getTime()`.
205    /// A value it cannot parse becomes `NaN`, which reaches SQL as the literal
206    /// string `"NaN"` and **matches nothing without erroring** — the query
207    /// returns HTTP 200 and an empty list. `"14/08/2023"` fails this way;
208    /// `"Aug 14 2023"` parses but depends on the server's local timezone.
209    ///
210    /// Prefer [`BlockQueryInput::date_time_gte_from_unix_ms`], which cannot
211    /// express an unparseable value.
212    #[serde(rename = "dateTime_gte", skip_serializing_if = "Option::is_none")]
213    pub date_time_gte: Option<String>,
214    /// Exclusive upper bound, as an ISO-8601 / RFC 3339 instant.
215    ///
216    /// Carries the same silent-empty-result hazard as
217    /// [`BlockQueryInput::date_time_gte`]. Prefer
218    /// [`BlockQueryInput::date_time_lt_from_unix_ms`].
219    #[serde(rename = "dateTime_lt", skip_serializing_if = "Option::is_none")]
220    pub date_time_lt: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub canonical: Option<bool>,
223    #[serde(skip_serializing_if = "Option::is_none", rename = "inBestChain")]
224    pub in_best_chain: Option<bool>,
225}
226
227impl BlockQueryInput {
228    /// Set the inclusive lower bound from Unix epoch milliseconds.
229    ///
230    /// Formats to ISO-8601 with a `Z` offset, which the server always parses
231    /// to a finite number — so this cannot produce the silent empty result
232    /// that a hand-written date string can.
233    pub fn date_time_gte_from_unix_ms(mut self, ms: i64) -> Self {
234        self.date_time_gte = Some(iso8601_from_unix_ms(ms));
235        self
236    }
237
238    /// Set the exclusive upper bound from Unix epoch milliseconds.
239    ///
240    /// See [`BlockQueryInput::date_time_gte_from_unix_ms`].
241    pub fn date_time_lt_from_unix_ms(mut self, ms: i64) -> Self {
242        self.date_time_lt = Some(iso8601_from_unix_ms(ms));
243        self
244    }
245}
246
247/// Format Unix epoch milliseconds as `YYYY-MM-DDTHH:MM:SS.sssZ`.
248///
249/// Hand-rolled because the crate carries no `chrono`/`time` dependency and
250/// this is the only place a date has to be built. Uses Howard Hinnant's
251/// `civil_from_days`, which is exact for the whole proleptic Gregorian range.
252fn iso8601_from_unix_ms(ms: i64) -> String {
253    const MS_PER_DAY: i64 = 86_400_000;
254    let days = ms.div_euclid(MS_PER_DAY);
255    let mut rem = ms.rem_euclid(MS_PER_DAY);
256
257    let millis = rem % 1000;
258    rem /= 1000;
259    let seconds = rem % 60;
260    rem /= 60;
261    let minutes = rem % 60;
262    let hours = rem / 60;
263
264    let (year, month, day) = civil_from_days(days);
265    format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z")
266}
267
268/// Days since 1970-01-01 to a (year, month, day) civil date.
269fn civil_from_days(days: i64) -> (i64, u32, u32) {
270    let z = days + 719_468;
271    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
272    let doe = z - era * 146_097; // [0, 146096]
273    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
274    let y = yoe + era * 400;
275    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
276    let mp = (5 * doy + 2) / 153; // [0, 11]
277    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
278    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
279    (if m <= 2 { y + 1 } else { y }, m as u32, d as u32)
280}
281
282#[derive(Debug, Clone, Deserialize)]
283#[serde(rename_all = "camelCase")]
284#[non_exhaustive]
285pub struct TransactionInfo {
286    pub status: String,
287    pub hash: String,
288    pub memo: String,
289    pub authorization_kind: String,
290    pub sequence_number: i64,
291    pub zkapp_account_update_ids: Vec<i64>,
292}
293
294#[derive(Debug, Clone, Deserialize)]
295#[serde(rename_all = "camelCase")]
296#[non_exhaustive]
297pub struct EventData {
298    pub account_update_id: String,
299    pub transaction_info: Option<TransactionInfo>,
300    pub data: Vec<String>,
301}
302
303#[derive(Debug, Clone, Deserialize)]
304#[serde(rename_all = "camelCase")]
305#[non_exhaustive]
306pub struct ActionData {
307    pub account_update_id: String,
308    pub transaction_info: Option<TransactionInfo>,
309    pub data: Vec<String>,
310}
311
312#[derive(Debug, Clone, Deserialize)]
313#[serde(rename_all = "camelCase")]
314#[non_exhaustive]
315pub struct BlockInfo {
316    pub height: i64,
317    pub state_hash: String,
318    pub parent_hash: String,
319    pub ledger_hash: String,
320    pub chain_status: String,
321    /// Unix epoch **milliseconds** as a decimal string, e.g. `"1692054601000"`.
322    ///
323    /// **Not** ISO-8601 — an RFC 3339 parser rejects it, and reading it as
324    /// seconds puts the block in 1970. Parse the integer first. Contrast
325    /// [`Block::date_time`], a few fields away, which *is* ISO-8601.
326    /// Unix time in **milliseconds** as a decimal string, e.g.
327    /// `"1692054601000"`.
328    ///
329    /// Not ISO-8601 — see [`Block::date_time`], which is the other encoding
330    /// this schema uses. Parsing this one with an RFC 3339 parser fails, and
331    /// reading it as seconds puts the block in the wrong century.
332    pub timestamp: String,
333    pub global_slot_since_hardfork: i64,
334    pub global_slot_since_genesis: i64,
335    pub distance_from_max_block_height: i64,
336}
337
338#[derive(Debug, Clone, Deserialize)]
339#[serde(rename_all = "camelCase")]
340#[non_exhaustive]
341pub struct ActionStates {
342    pub action_state_one: Option<String>,
343    pub action_state_two: Option<String>,
344    pub action_state_three: Option<String>,
345    pub action_state_four: Option<String>,
346    pub action_state_five: Option<String>,
347}
348
349#[derive(Debug, Clone, Deserialize)]
350#[serde(rename_all = "camelCase")]
351#[non_exhaustive]
352pub struct EventOutput {
353    pub block_info: Option<BlockInfo>,
354    /// `[EventData!]` in the SDL: the list may be `null`, its elements may not.
355    pub event_data: Option<Vec<EventData>>,
356}
357
358#[derive(Debug, Clone, Deserialize)]
359#[serde(rename_all = "camelCase")]
360#[non_exhaustive]
361pub struct ActionOutput {
362    pub block_info: Option<BlockInfo>,
363    pub transaction_info: Option<TransactionInfo>,
364    /// `[ActionData!]` in the SDL: the list may be `null`, its elements may not.
365    pub action_data: Option<Vec<ActionData>>,
366    pub action_state: ActionStates,
367}
368
369/// An applied account update that set a verification key.
370#[derive(Debug, Clone, Deserialize)]
371#[serde(rename_all = "camelCase")]
372#[non_exhaustive]
373pub struct VerificationKeyUpdate {
374    pub account_update_id: String,
375    /// The account whose verification key was set.
376    pub address: String,
377    pub token_id: String,
378    pub verification_key_hash: String,
379    pub block_info: BlockInfo,
380    pub transaction_info: TransactionInfo,
381}
382
383#[derive(Debug, Clone, Deserialize)]
384#[serde(rename_all = "camelCase")]
385#[non_exhaustive]
386pub struct MaxBlockHeightInfo {
387    pub canonical_max_block_height: i64,
388    pub pending_max_block_height: i64,
389}
390
391#[derive(Debug, Clone, Deserialize)]
392#[serde(rename_all = "camelCase")]
393#[non_exhaustive]
394pub struct NetworkStateOutput {
395    pub max_block_height: Option<MaxBlockHeightInfo>,
396}
397
398#[derive(Debug, Clone, Deserialize)]
399#[non_exhaustive]
400pub struct UserCommand {
401    pub hash: String,
402    pub kind: String,
403    pub from: String,
404    pub to: String,
405    pub amount: String,
406    pub fee: String,
407    pub memo: String,
408    pub nonce: i64,
409    pub status: String,
410    #[serde(rename = "failureReason")]
411    pub failure_reason: Option<String>,
412}
413
414#[derive(Debug, Clone, Deserialize)]
415#[serde(rename_all = "camelCase")]
416#[non_exhaustive]
417pub struct ZkAppCommand {
418    pub hash: String,
419    pub fee_payer: String,
420    pub fee: String,
421    pub memo: String,
422    pub status: String,
423    pub failure_reason: Option<String>,
424}
425
426#[derive(Debug, Clone, Deserialize)]
427#[non_exhaustive]
428pub struct FeeTransfer {
429    pub recipient: String,
430    pub fee: String,
431    #[serde(rename = "type")]
432    pub kind: String,
433}
434
435/// Transactions attached to a block.
436///
437/// Only `coinbase` is populated by a stock server. The other three fields
438/// require `ENABLE_BLOCK_TRANSACTION_DETAILS=true`, which defaults to `false`,
439/// and are empty otherwise. `Block::parent_hash` is `""` under the same flag.
440#[derive(Debug, Clone, Deserialize)]
441#[serde(rename_all = "camelCase")]
442#[non_exhaustive]
443pub struct BlockTransactions {
444    pub coinbase: String,
445    pub user_commands: Vec<UserCommand>,
446    pub zkapp_commands: Vec<ZkAppCommand>,
447    pub fee_transfer: Vec<FeeTransfer>,
448}
449
450#[derive(Debug, Clone, Deserialize)]
451#[serde(rename_all = "camelCase")]
452#[non_exhaustive]
453pub struct Block {
454    pub block_height: i64,
455    pub creator: String,
456    pub state_hash: String,
457    pub parent_hash: String,
458    /// ISO-8601 / RFC 3339 instant, e.g. `"2023-08-14T23:10:01.000Z"`.
459    ///
460    /// The server derives it from the same archive column that
461    /// [`BlockInfo::timestamp`] exposes raw, so the two describe the same
462    /// instant in two different encodings: this one is ISO-8601, that one is
463    /// Unix milliseconds as a decimal string, despite the identical Rust type.
464    pub date_time: String,
465    pub transactions: BlockTransactions,
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    /// The values in #13's evidence table, plus the epoch and a pre-epoch
473    /// instant to exercise the floor-division path.
474    #[test]
475    fn iso8601_from_unix_ms_matches_javascript_to_iso_string() {
476        let cases: &[(i64, &str)] = &[
477            (1_691_971_200_000, "2023-08-14T00:00:00.000Z"),
478            (1_692_054_601_000, "2023-08-14T23:10:01.000Z"),
479            (0, "1970-01-01T00:00:00.000Z"),
480            (-1, "1969-12-31T23:59:59.999Z"),
481            (951_782_400_000, "2000-02-29T00:00:00.000Z"),
482            (4_102_444_800_000, "2100-01-01T00:00:00.000Z"),
483        ];
484        for (ms, expected) in cases {
485            assert_eq!(&iso8601_from_unix_ms(*ms), expected, "for {ms} ms");
486        }
487    }
488
489    /// The point of the typed constructors: the string they emit is one the
490    /// server's `new Date(v).getTime()` turns back into the same finite
491    /// number, so the filter cannot silently match nothing.
492    #[test]
493    fn typed_constructors_round_trip_through_the_wire_format() {
494        let input = BlockQueryInput::default()
495            .date_time_gte_from_unix_ms(1_691_971_200_000)
496            .date_time_lt_from_unix_ms(1_692_054_601_000);
497
498        let json = serde_json::to_value(&input).unwrap();
499        assert_eq!(json["dateTime_gte"], "2023-08-14T00:00:00.000Z");
500        assert_eq!(json["dateTime_lt"], "2023-08-14T23:10:01.000Z");
501    }
502}