Skip to main content

linera_chain/data_types/
metadata.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! GraphQL-compatible structured metadata representations for operations and messages.
5
6// The GraphQL spec only has signed integer scalars, so this module casts
7// `u32` to `i32` at the API boundary. The casts are by design.
8#![allow(clippy::cast_possible_wrap)]
9
10use async_graphql::SimpleObject;
11use linera_base::{
12    crypto::CryptoHash,
13    data_types::{Amount, ApplicationPermissions, Cursor},
14    hex,
15    identifiers::{Account, AccountOwner, ApplicationId, ChainId},
16    ownership::{ChainOwnership, TimeoutConfig},
17};
18use linera_execution::{system::AdminOperation, Message, SystemMessage, SystemOperation};
19use serde::{Deserialize, Serialize};
20
21/// Timeout configuration metadata for GraphQL.
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
23pub struct TimeoutConfigMetadata {
24    /// The duration of the fast round in milliseconds.
25    pub fast_round_ms: Option<String>,
26    /// The duration of the first single-leader and all multi-leader rounds in milliseconds.
27    pub base_timeout_ms: String,
28    /// The duration by which the timeout increases after each single-leader round in milliseconds.
29    pub timeout_increment_ms: String,
30    /// The age of an incoming tracked or protected message after which validators start
31    /// transitioning to fallback mode, in milliseconds.
32    pub fallback_duration_ms: String,
33}
34
35impl From<&TimeoutConfig> for TimeoutConfigMetadata {
36    fn from(config: &TimeoutConfig) -> Self {
37        TimeoutConfigMetadata {
38            fast_round_ms: config
39                .fast_round_duration
40                .map(|d| (d.as_micros() / 1000).to_string()),
41            base_timeout_ms: (config.base_timeout.as_micros() / 1000).to_string(),
42            timeout_increment_ms: (config.timeout_increment.as_micros() / 1000).to_string(),
43            fallback_duration_ms: (config.fallback_duration.as_micros() / 1000).to_string(),
44        }
45    }
46}
47
48/// Chain ownership metadata for GraphQL.
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
50pub struct ChainOwnershipMetadata {
51    /// JSON serialized `ChainOwnership` for full representation.
52    pub ownership_json: String,
53}
54
55impl From<&ChainOwnership> for ChainOwnershipMetadata {
56    fn from(ownership: &ChainOwnership) -> Self {
57        ChainOwnershipMetadata {
58            // Fallback to Debug format should never be needed, as ChainOwnership implements Serialize.
59            // But we include it as a safety measure for GraphQL responses to always succeed.
60            ownership_json: serde_json::to_string(ownership)
61                .unwrap_or_else(|_| format!("{ownership:?}")),
62        }
63    }
64}
65
66/// Application permissions metadata for GraphQL.
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
68pub struct ApplicationPermissionsMetadata {
69    /// JSON serialized `ApplicationPermissions`.
70    pub permissions_json: String,
71}
72
73impl From<&ApplicationPermissions> for ApplicationPermissionsMetadata {
74    fn from(permissions: &ApplicationPermissions) -> Self {
75        ApplicationPermissionsMetadata {
76            // Fallback to Debug format should never be needed, as ApplicationPermissions implements Serialize.
77            // But we include it as a safety measure for GraphQL responses to always succeed.
78            permissions_json: serde_json::to_string(permissions)
79                .unwrap_or_else(|_| format!("{permissions:?}")),
80        }
81    }
82}
83
84/// Structured representation of a system operation for GraphQL.
85#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
86pub struct SystemOperationMetadata {
87    /// The type of system operation
88    pub system_operation_type: String,
89    /// Transfer operation details
90    pub transfer: Option<TransferOperationMetadata>,
91    /// Claim operation details
92    pub claim: Option<ClaimOperationMetadata>,
93    /// Open chain operation details
94    pub open_chain: Option<OpenChainOperationMetadata>,
95    /// Change ownership operation details
96    pub change_ownership: Option<ChangeOwnershipOperationMetadata>,
97    /// Change application permissions operation details
98    pub change_application_permissions: Option<ChangeApplicationPermissionsMetadata>,
99    /// Admin operation details
100    pub admin: Option<AdminOperationMetadata>,
101    /// Create application operation details
102    pub create_application: Option<CreateApplicationOperationMetadata>,
103    /// Publish data blob operation details
104    pub publish_data_blob: Option<PublishDataBlobMetadata>,
105    /// Verify blob operation details
106    pub verify_blob: Option<VerifyBlobMetadata>,
107    /// Publish module operation details
108    pub publish_module: Option<PublishModuleMetadata>,
109    /// Epoch operation details (`ProcessNewEpoch`)
110    pub epoch: Option<i32>,
111    /// `UpdateStream` operation details
112    pub update_stream: Option<UpdateStreamMetadata>,
113}
114
115impl SystemOperationMetadata {
116    /// Creates a new metadata with the given operation type and all fields set to `None`.
117    fn new(system_operation_type: &str) -> Self {
118        SystemOperationMetadata {
119            system_operation_type: system_operation_type.to_string(),
120            transfer: None,
121            claim: None,
122            open_chain: None,
123            change_ownership: None,
124            change_application_permissions: None,
125            admin: None,
126            create_application: None,
127            publish_data_blob: None,
128            verify_blob: None,
129            publish_module: None,
130            epoch: None,
131            update_stream: None,
132        }
133    }
134}
135
136/// Transfer operation metadata.
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
138pub struct TransferOperationMetadata {
139    /// The account owner whose balance is debited.
140    pub owner: AccountOwner,
141    /// The account that receives the transferred tokens.
142    pub recipient: Account,
143    /// The amount of tokens to transfer.
144    pub amount: Amount,
145}
146
147/// Claim operation metadata.
148#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
149pub struct ClaimOperationMetadata {
150    /// The account owner whose balance is being claimed.
151    pub owner: AccountOwner,
152    /// The chain on which the claimed balance is held.
153    pub target_id: ChainId,
154    /// The account that receives the claimed tokens.
155    pub recipient: Account,
156    /// The amount of tokens to claim.
157    pub amount: Amount,
158}
159
160/// Open chain operation metadata.
161#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
162pub struct OpenChainOperationMetadata {
163    /// The initial balance credited to the new chain.
164    pub balance: Amount,
165    /// The ownership configuration of the new chain.
166    pub ownership: ChainOwnershipMetadata,
167    /// The application permissions of the new chain.
168    pub application_permissions: ApplicationPermissionsMetadata,
169}
170
171/// Change ownership operation metadata.
172#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
173pub struct ChangeOwnershipOperationMetadata {
174    /// The super owners, who can propose fast blocks in the first round and regular blocks in any round.
175    pub super_owners: Vec<AccountOwner>,
176    /// The regular owners, each with the weight that determines how often they are round leader.
177    pub owners: Vec<OwnerWithWeight>,
178    /// The leader of the first single-leader round; if unset, that leader is random like other rounds.
179    pub first_leader: Option<AccountOwner>,
180    /// The number of rounds in which all owners are allowed to propose blocks.
181    pub multi_leader_rounds: i32,
182    /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
183    pub open_multi_leader_rounds: bool,
184    /// The timeout configuration governing round durations.
185    pub timeout_config: TimeoutConfigMetadata,
186}
187
188/// Owner with weight metadata.
189#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
190pub struct OwnerWithWeight {
191    /// The account owner.
192    pub owner: AccountOwner,
193    /// The owner's weight, determining how often they are round leader (a `u64` as a string).
194    pub weight: String, // Using String to represent u64 safely in GraphQL
195}
196
197/// Change application permissions operation metadata.
198#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
199pub struct ChangeApplicationPermissionsMetadata {
200    /// The new application permissions to set on the chain.
201    pub permissions: ApplicationPermissionsMetadata,
202}
203
204/// Admin operation metadata.
205#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
206pub struct AdminOperationMetadata {
207    /// The kind of admin operation: "PublishCommitteeBlob", "CreateCommittee" or "RemoveCommittee".
208    pub admin_operation_type: String,
209    /// The committee epoch this operation refers to, if applicable.
210    pub epoch: Option<i32>,
211    /// The hash of the committee blob, if applicable.
212    pub blob_hash: Option<CryptoHash>,
213}
214
215/// Create application operation metadata.
216#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
217pub struct CreateApplicationOperationMetadata {
218    /// The ID of the module the application is instantiated from.
219    pub module_id: String,
220    /// The application's static parameters, encoded as a hex string.
221    pub parameters_hex: String,
222    /// The argument passed to the application's instantiation, encoded as a hex string.
223    pub instantiation_argument_hex: String,
224    /// The applications this application depends on and requires to be present.
225    pub required_application_ids: Vec<ApplicationId>,
226}
227
228/// Publish data blob operation metadata.
229#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
230pub struct PublishDataBlobMetadata {
231    /// The hash of the data blob being published.
232    pub blob_hash: CryptoHash,
233}
234
235/// Verify blob operation metadata.
236#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
237pub struct VerifyBlobMetadata {
238    /// The ID of the blob whose existence is being verified.
239    pub blob_id: String,
240}
241
242/// Publish module operation metadata.
243#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
244pub struct PublishModuleMetadata {
245    /// The ID of the module being published.
246    pub module_id: String,
247}
248
249/// Update stream metadata.
250#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
251pub struct UpdateStreamMetadata {
252    /// The application that owns the event stream.
253    pub application_id: String,
254    /// The chain on which the events are published.
255    pub chain_id: ChainId,
256    /// The identifier of the event stream being updated.
257    pub stream_id: String,
258    /// The lowest event index still guaranteed to be readable (if it exists).
259    pub first_index: i32,
260    /// The index of the next event to read from the stream.
261    pub next_index: i32,
262}
263
264/// Structured representation of a system message for GraphQL.
265#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
266pub struct SystemMessageMetadata {
267    /// The type of system message
268    pub system_message_type: String,
269    /// Credit message details
270    pub credit: Option<CreditMessageMetadata>,
271    /// Withdraw message details
272    pub withdraw: Option<WithdrawMessageMetadata>,
273    /// CheckpointAck message details
274    pub checkpoint_ack: Option<CheckpointAckMessageMetadata>,
275}
276
277/// Credit message metadata.
278#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
279pub struct CreditMessageMetadata {
280    /// The account owner whose balance is credited on the receiving chain.
281    pub target: AccountOwner,
282    /// The amount of tokens being credited.
283    pub amount: Amount,
284    /// The account owner the transfer originated from.
285    pub source: AccountOwner,
286}
287
288/// Withdraw message metadata.
289#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
290pub struct WithdrawMessageMetadata {
291    /// The account owner whose balance is debited on the source chain.
292    pub owner: AccountOwner,
293    /// The amount of tokens being withdrawn.
294    pub amount: Amount,
295    /// The account that receives the withdrawn tokens.
296    pub recipient: Account,
297}
298
299/// CheckpointAck message metadata.
300#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
301pub struct CheckpointAckMessageMetadata {
302    /// The cursor past the last bundle from the recipient that the sender has consumed.
303    pub latest_received_cursor: Cursor,
304}
305
306/// Structured representation of a message for GraphQL.
307#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
308pub struct MessageMetadata {
309    /// The type of message: "System" or "User"
310    pub message_type: String,
311    /// For user messages, the application ID
312    pub application_id: Option<ApplicationId>,
313    /// For user messages, the serialized bytes (as a hex string for GraphQL)
314    pub user_bytes_hex: Option<String>,
315    /// For system messages, structured representation
316    pub system_message: Option<SystemMessageMetadata>,
317}
318
319impl From<&SystemOperation> for SystemOperationMetadata {
320    fn from(sys_op: &SystemOperation) -> Self {
321        match sys_op {
322            SystemOperation::Transfer {
323                owner,
324                recipient,
325                amount,
326            } => SystemOperationMetadata {
327                transfer: Some(TransferOperationMetadata {
328                    owner: *owner,
329                    recipient: *recipient,
330                    amount: *amount,
331                }),
332                ..SystemOperationMetadata::new("Transfer")
333            },
334            SystemOperation::Claim {
335                owner,
336                target_id,
337                recipient,
338                amount,
339            } => SystemOperationMetadata {
340                claim: Some(ClaimOperationMetadata {
341                    owner: *owner,
342                    target_id: *target_id,
343                    recipient: *recipient,
344                    amount: *amount,
345                }),
346                ..SystemOperationMetadata::new("Claim")
347            },
348            SystemOperation::OpenChain(config) => SystemOperationMetadata {
349                open_chain: Some(OpenChainOperationMetadata {
350                    balance: config.balance,
351                    ownership: ChainOwnershipMetadata::from(&config.ownership),
352                    application_permissions: ApplicationPermissionsMetadata::from(
353                        &config.application_permissions,
354                    ),
355                }),
356                ..SystemOperationMetadata::new("OpenChain")
357            },
358            SystemOperation::CloseChain => SystemOperationMetadata::new("CloseChain"),
359            SystemOperation::ChangeOwnership {
360                super_owners,
361                owners,
362                first_leader,
363                multi_leader_rounds,
364                open_multi_leader_rounds,
365                timeout_config,
366            } => SystemOperationMetadata {
367                change_ownership: Some(ChangeOwnershipOperationMetadata {
368                    super_owners: super_owners.clone(),
369                    owners: owners
370                        .iter()
371                        .map(|(owner, weight)| OwnerWithWeight {
372                            owner: *owner,
373                            weight: weight.to_string(),
374                        })
375                        .collect(),
376                    first_leader: *first_leader,
377                    multi_leader_rounds: *multi_leader_rounds as i32,
378                    open_multi_leader_rounds: *open_multi_leader_rounds,
379                    timeout_config: TimeoutConfigMetadata::from(timeout_config),
380                }),
381                ..SystemOperationMetadata::new("ChangeOwnership")
382            },
383            SystemOperation::ChangeApplicationPermissions(permissions) => SystemOperationMetadata {
384                change_application_permissions: Some(ChangeApplicationPermissionsMetadata {
385                    permissions: ApplicationPermissionsMetadata::from(permissions),
386                }),
387                ..SystemOperationMetadata::new("ChangeApplicationPermissions")
388            },
389            SystemOperation::Admin(admin_op) => SystemOperationMetadata {
390                admin: Some(AdminOperationMetadata::from(admin_op)),
391                ..SystemOperationMetadata::new("Admin")
392            },
393            SystemOperation::CreateApplication {
394                module_id,
395                parameters,
396                instantiation_argument,
397                required_application_ids,
398            } => SystemOperationMetadata {
399                create_application: Some(CreateApplicationOperationMetadata {
400                    module_id: module_id.to_string(),
401                    parameters_hex: hex::encode(parameters),
402                    instantiation_argument_hex: hex::encode(instantiation_argument),
403                    required_application_ids: required_application_ids.clone(),
404                }),
405                ..SystemOperationMetadata::new("CreateApplication")
406            },
407            SystemOperation::PublishDataBlob { blob_hash } => SystemOperationMetadata {
408                publish_data_blob: Some(PublishDataBlobMetadata {
409                    blob_hash: *blob_hash,
410                }),
411                ..SystemOperationMetadata::new("PublishDataBlob")
412            },
413            SystemOperation::VerifyBlob { blob_id } => SystemOperationMetadata {
414                verify_blob: Some(VerifyBlobMetadata {
415                    blob_id: blob_id.to_string(),
416                }),
417                ..SystemOperationMetadata::new("VerifyBlob")
418            },
419            SystemOperation::PublishModule { module_id } => SystemOperationMetadata {
420                publish_module: Some(PublishModuleMetadata {
421                    module_id: module_id.to_string(),
422                }),
423                ..SystemOperationMetadata::new("PublishModule")
424            },
425            SystemOperation::ProcessNewEpoch(epoch) => SystemOperationMetadata {
426                epoch: Some(epoch.0 as i32),
427                ..SystemOperationMetadata::new("ProcessNewEpoch")
428            },
429            SystemOperation::UpdateStream {
430                application_id,
431                chain_id,
432                stream_id,
433                first_index,
434                next_index,
435            } => SystemOperationMetadata {
436                update_stream: Some(UpdateStreamMetadata {
437                    application_id: application_id.to_string(),
438                    chain_id: *chain_id,
439                    stream_id: stream_id.to_string(),
440                    first_index: *first_index as i32,
441                    next_index: *next_index as i32,
442                }),
443                ..SystemOperationMetadata::new("UpdateStream")
444            },
445            SystemOperation::Checkpoint => SystemOperationMetadata::new("Checkpoint"),
446        }
447    }
448}
449
450impl From<&AdminOperation> for AdminOperationMetadata {
451    fn from(admin_op: &AdminOperation) -> Self {
452        match admin_op {
453            AdminOperation::PublishCommitteeBlob { blob_hash } => AdminOperationMetadata {
454                admin_operation_type: "PublishCommitteeBlob".to_string(),
455                epoch: None,
456                blob_hash: Some(*blob_hash),
457            },
458            AdminOperation::CreateCommittee { epoch, blob_hash } => AdminOperationMetadata {
459                admin_operation_type: "CreateCommittee".to_string(),
460                epoch: Some(epoch.0 as i32),
461                blob_hash: Some(*blob_hash),
462            },
463            AdminOperation::RemoveCommittee { epoch } => AdminOperationMetadata {
464                admin_operation_type: "RemoveCommittee".to_string(),
465                epoch: Some(epoch.0 as i32),
466                blob_hash: None,
467            },
468        }
469    }
470}
471
472impl From<&Message> for MessageMetadata {
473    fn from(message: &Message) -> Self {
474        match message {
475            Message::System(sys_msg) => MessageMetadata {
476                message_type: "System".to_string(),
477                application_id: None,
478                user_bytes_hex: None,
479                system_message: Some(SystemMessageMetadata::from(sys_msg)),
480            },
481            Message::User {
482                application_id,
483                bytes,
484            } => MessageMetadata {
485                message_type: "User".to_string(),
486                application_id: Some(*application_id),
487                user_bytes_hex: Some(hex::encode(bytes)),
488                system_message: None,
489            },
490        }
491    }
492}
493
494impl From<&SystemMessage> for SystemMessageMetadata {
495    fn from(sys_msg: &SystemMessage) -> Self {
496        match sys_msg {
497            SystemMessage::Credit {
498                target,
499                amount,
500                source,
501            } => SystemMessageMetadata {
502                system_message_type: "Credit".to_string(),
503                credit: Some(CreditMessageMetadata {
504                    target: *target,
505                    amount: *amount,
506                    source: *source,
507                }),
508                withdraw: None,
509                checkpoint_ack: None,
510            },
511            SystemMessage::Withdraw {
512                owner,
513                amount,
514                recipient,
515            } => SystemMessageMetadata {
516                system_message_type: "Withdraw".to_string(),
517                credit: None,
518                withdraw: Some(WithdrawMessageMetadata {
519                    owner: *owner,
520                    amount: *amount,
521                    recipient: *recipient,
522                }),
523                checkpoint_ack: None,
524            },
525            SystemMessage::CheckpointAck {
526                latest_received_cursor,
527            } => SystemMessageMetadata {
528                system_message_type: "CheckpointAck".to_string(),
529                credit: None,
530                withdraw: None,
531                checkpoint_ack: Some(CheckpointAckMessageMetadata {
532                    latest_received_cursor: *latest_received_cursor,
533                }),
534            },
535        }
536    }
537}