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 account on the new chain credited with the initial balance.
164    pub account: AccountOwner,
165    /// The initial balance credited to `account`.
166    pub balance: Amount,
167    /// The ownership configuration of the new chain.
168    pub ownership: ChainOwnershipMetadata,
169    /// The application permissions of the new chain.
170    pub application_permissions: ApplicationPermissionsMetadata,
171}
172
173/// Change ownership operation metadata.
174#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
175pub struct ChangeOwnershipOperationMetadata {
176    /// The super owners, who can propose fast blocks in the first round and regular blocks in any round.
177    pub super_owners: Vec<AccountOwner>,
178    /// The regular owners, each with the weight that determines how often they are round leader.
179    pub owners: Vec<OwnerWithWeight>,
180    /// The leader of the first single-leader round; if unset, that leader is random like other rounds.
181    pub first_leader: Option<AccountOwner>,
182    /// The number of rounds in which all owners are allowed to propose blocks.
183    pub multi_leader_rounds: i32,
184    /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
185    pub open_multi_leader_rounds: bool,
186    /// The timeout configuration governing round durations.
187    pub timeout_config: TimeoutConfigMetadata,
188}
189
190/// Owner with weight metadata.
191#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
192pub struct OwnerWithWeight {
193    /// The account owner.
194    pub owner: AccountOwner,
195    /// The owner's weight, determining how often they are round leader (a `u64` as a string).
196    pub weight: String, // Using String to represent u64 safely in GraphQL
197}
198
199/// Change application permissions operation metadata.
200#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
201pub struct ChangeApplicationPermissionsMetadata {
202    /// The new application permissions to set on the chain.
203    pub permissions: ApplicationPermissionsMetadata,
204}
205
206/// Admin operation metadata.
207#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
208pub struct AdminOperationMetadata {
209    /// The kind of admin operation: "PublishCommitteeBlob", "CreateCommittee" or "RemoveCommittee".
210    pub admin_operation_type: String,
211    /// The committee epoch this operation refers to, if applicable.
212    pub epoch: Option<i32>,
213    /// The hash of the committee blob, if applicable.
214    pub blob_hash: Option<CryptoHash>,
215}
216
217/// Create application operation metadata.
218#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
219pub struct CreateApplicationOperationMetadata {
220    /// The ID of the module the application is instantiated from.
221    pub module_id: String,
222    /// The application's static parameters, encoded as a hex string.
223    pub parameters_hex: String,
224    /// The argument passed to the application's instantiation, encoded as a hex string.
225    pub instantiation_argument_hex: String,
226    /// The applications this application depends on and requires to be present.
227    pub required_application_ids: Vec<ApplicationId>,
228}
229
230/// Publish data blob operation metadata.
231#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
232pub struct PublishDataBlobMetadata {
233    /// The hash of the data blob being published.
234    pub blob_hash: CryptoHash,
235}
236
237/// Verify blob operation metadata.
238#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
239pub struct VerifyBlobMetadata {
240    /// The ID of the blob whose existence is being verified.
241    pub blob_id: String,
242}
243
244/// Publish module operation metadata.
245#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
246pub struct PublishModuleMetadata {
247    /// The ID of the module being published.
248    pub module_id: String,
249}
250
251/// Update stream metadata.
252#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
253pub struct UpdateStreamMetadata {
254    /// The application that owns the event stream.
255    pub application_id: String,
256    /// The chain on which the events are published.
257    pub chain_id: ChainId,
258    /// The identifier of the event stream being updated.
259    pub stream_id: String,
260    /// The lowest event index still guaranteed to be readable (if it exists).
261    pub first_index: i32,
262    /// The index of the next event to read from the stream.
263    pub next_index: i32,
264}
265
266/// Structured representation of a system message for GraphQL.
267#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
268pub struct SystemMessageMetadata {
269    /// The type of system message
270    pub system_message_type: String,
271    /// Credit message details
272    pub credit: Option<CreditMessageMetadata>,
273    /// Withdraw message details
274    pub withdraw: Option<WithdrawMessageMetadata>,
275    /// CheckpointAck message details
276    pub checkpoint_ack: Option<CheckpointAckMessageMetadata>,
277}
278
279/// Credit message metadata.
280#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
281pub struct CreditMessageMetadata {
282    /// The account owner whose balance is credited on the receiving chain.
283    pub target: AccountOwner,
284    /// The amount of tokens being credited.
285    pub amount: Amount,
286    /// The account owner the transfer originated from.
287    pub source: AccountOwner,
288}
289
290/// Withdraw message metadata.
291#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
292pub struct WithdrawMessageMetadata {
293    /// The account owner whose balance is debited on the source chain.
294    pub owner: AccountOwner,
295    /// The amount of tokens being withdrawn.
296    pub amount: Amount,
297    /// The account that receives the withdrawn tokens.
298    pub recipient: Account,
299}
300
301/// CheckpointAck message metadata.
302#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
303pub struct CheckpointAckMessageMetadata {
304    /// The cursor past the last bundle from the recipient that the sender has consumed.
305    pub latest_received_cursor: Cursor,
306}
307
308/// Structured representation of a message for GraphQL.
309#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
310pub struct MessageMetadata {
311    /// The type of message: "System" or "User"
312    pub message_type: String,
313    /// For user messages, the application ID
314    pub application_id: Option<ApplicationId>,
315    /// For user messages, the serialized bytes (as a hex string for GraphQL)
316    pub user_bytes_hex: Option<String>,
317    /// For system messages, structured representation
318    pub system_message: Option<SystemMessageMetadata>,
319}
320
321impl From<&SystemOperation> for SystemOperationMetadata {
322    fn from(sys_op: &SystemOperation) -> Self {
323        match sys_op {
324            SystemOperation::Transfer {
325                owner,
326                recipient,
327                amount,
328            } => SystemOperationMetadata {
329                transfer: Some(TransferOperationMetadata {
330                    owner: *owner,
331                    recipient: *recipient,
332                    amount: *amount,
333                }),
334                ..SystemOperationMetadata::new("Transfer")
335            },
336            SystemOperation::Claim {
337                owner,
338                target_id,
339                recipient,
340                amount,
341            } => SystemOperationMetadata {
342                claim: Some(ClaimOperationMetadata {
343                    owner: *owner,
344                    target_id: *target_id,
345                    recipient: *recipient,
346                    amount: *amount,
347                }),
348                ..SystemOperationMetadata::new("Claim")
349            },
350            SystemOperation::OpenChain(config) => SystemOperationMetadata {
351                open_chain: Some(OpenChainOperationMetadata {
352                    account: config.account,
353                    balance: config.balance,
354                    ownership: ChainOwnershipMetadata::from(&config.ownership),
355                    application_permissions: ApplicationPermissionsMetadata::from(
356                        &config.application_permissions,
357                    ),
358                }),
359                ..SystemOperationMetadata::new("OpenChain")
360            },
361            SystemOperation::CloseChain => SystemOperationMetadata::new("CloseChain"),
362            SystemOperation::ChangeOwnership {
363                super_owners,
364                owners,
365                first_leader,
366                multi_leader_rounds,
367                open_multi_leader_rounds,
368                timeout_config,
369            } => SystemOperationMetadata {
370                change_ownership: Some(ChangeOwnershipOperationMetadata {
371                    super_owners: super_owners.clone(),
372                    owners: owners
373                        .iter()
374                        .map(|(owner, weight)| OwnerWithWeight {
375                            owner: *owner,
376                            weight: weight.to_string(),
377                        })
378                        .collect(),
379                    first_leader: *first_leader,
380                    multi_leader_rounds: *multi_leader_rounds as i32,
381                    open_multi_leader_rounds: *open_multi_leader_rounds,
382                    timeout_config: TimeoutConfigMetadata::from(timeout_config),
383                }),
384                ..SystemOperationMetadata::new("ChangeOwnership")
385            },
386            SystemOperation::ChangeApplicationPermissions(permissions) => SystemOperationMetadata {
387                change_application_permissions: Some(ChangeApplicationPermissionsMetadata {
388                    permissions: ApplicationPermissionsMetadata::from(permissions),
389                }),
390                ..SystemOperationMetadata::new("ChangeApplicationPermissions")
391            },
392            SystemOperation::Admin(admin_op) => SystemOperationMetadata {
393                admin: Some(AdminOperationMetadata::from(admin_op)),
394                ..SystemOperationMetadata::new("Admin")
395            },
396            SystemOperation::CreateApplication {
397                module_id,
398                parameters,
399                instantiation_argument,
400                required_application_ids,
401            } => SystemOperationMetadata {
402                create_application: Some(CreateApplicationOperationMetadata {
403                    module_id: module_id.to_string(),
404                    parameters_hex: hex::encode(parameters),
405                    instantiation_argument_hex: hex::encode(instantiation_argument),
406                    required_application_ids: required_application_ids.clone(),
407                }),
408                ..SystemOperationMetadata::new("CreateApplication")
409            },
410            SystemOperation::PublishDataBlob { blob_hash } => SystemOperationMetadata {
411                publish_data_blob: Some(PublishDataBlobMetadata {
412                    blob_hash: *blob_hash,
413                }),
414                ..SystemOperationMetadata::new("PublishDataBlob")
415            },
416            SystemOperation::VerifyBlob { blob_id } => SystemOperationMetadata {
417                verify_blob: Some(VerifyBlobMetadata {
418                    blob_id: blob_id.to_string(),
419                }),
420                ..SystemOperationMetadata::new("VerifyBlob")
421            },
422            SystemOperation::PublishModule { module_id } => SystemOperationMetadata {
423                publish_module: Some(PublishModuleMetadata {
424                    module_id: module_id.to_string(),
425                }),
426                ..SystemOperationMetadata::new("PublishModule")
427            },
428            SystemOperation::ProcessNewEpoch(epoch) => SystemOperationMetadata {
429                epoch: Some(epoch.0 as i32),
430                ..SystemOperationMetadata::new("ProcessNewEpoch")
431            },
432            SystemOperation::UpdateStream {
433                application_id,
434                chain_id,
435                stream_id,
436                first_index,
437                next_index,
438            } => SystemOperationMetadata {
439                update_stream: Some(UpdateStreamMetadata {
440                    application_id: application_id.to_string(),
441                    chain_id: *chain_id,
442                    stream_id: stream_id.to_string(),
443                    first_index: *first_index as i32,
444                    next_index: *next_index as i32,
445                }),
446                ..SystemOperationMetadata::new("UpdateStream")
447            },
448            SystemOperation::Checkpoint => SystemOperationMetadata::new("Checkpoint"),
449        }
450    }
451}
452
453impl From<&AdminOperation> for AdminOperationMetadata {
454    fn from(admin_op: &AdminOperation) -> Self {
455        match admin_op {
456            AdminOperation::PublishCommitteeBlob { blob_hash } => AdminOperationMetadata {
457                admin_operation_type: "PublishCommitteeBlob".to_string(),
458                epoch: None,
459                blob_hash: Some(*blob_hash),
460            },
461            AdminOperation::CreateCommittee { epoch, blob_hash } => AdminOperationMetadata {
462                admin_operation_type: "CreateCommittee".to_string(),
463                epoch: Some(epoch.0 as i32),
464                blob_hash: Some(*blob_hash),
465            },
466            AdminOperation::RemoveCommittee { epoch } => AdminOperationMetadata {
467                admin_operation_type: "RemoveCommittee".to_string(),
468                epoch: Some(epoch.0 as i32),
469                blob_hash: None,
470            },
471        }
472    }
473}
474
475impl From<&Message> for MessageMetadata {
476    fn from(message: &Message) -> Self {
477        match message {
478            Message::System(sys_msg) => MessageMetadata {
479                message_type: "System".to_string(),
480                application_id: None,
481                user_bytes_hex: None,
482                system_message: Some(SystemMessageMetadata::from(sys_msg)),
483            },
484            Message::User {
485                application_id,
486                bytes,
487            } => MessageMetadata {
488                message_type: "User".to_string(),
489                application_id: Some(*application_id),
490                user_bytes_hex: Some(hex::encode(bytes)),
491                system_message: None,
492            },
493        }
494    }
495}
496
497impl From<&SystemMessage> for SystemMessageMetadata {
498    fn from(sys_msg: &SystemMessage) -> Self {
499        match sys_msg {
500            SystemMessage::Credit {
501                target,
502                amount,
503                source,
504            } => SystemMessageMetadata {
505                system_message_type: "Credit".to_string(),
506                credit: Some(CreditMessageMetadata {
507                    target: *target,
508                    amount: *amount,
509                    source: *source,
510                }),
511                withdraw: None,
512                checkpoint_ack: None,
513            },
514            SystemMessage::Withdraw {
515                owner,
516                amount,
517                recipient,
518            } => SystemMessageMetadata {
519                system_message_type: "Withdraw".to_string(),
520                credit: None,
521                withdraw: Some(WithdrawMessageMetadata {
522                    owner: *owner,
523                    amount: *amount,
524                    recipient: *recipient,
525                }),
526                checkpoint_ack: None,
527            },
528            SystemMessage::CheckpointAck {
529                latest_received_cursor,
530            } => SystemMessageMetadata {
531                system_message_type: "CheckpointAck".to_string(),
532                credit: None,
533                withdraw: None,
534                checkpoint_ack: Some(CheckpointAckMessageMetadata {
535                    latest_received_cursor: *latest_received_cursor,
536                }),
537            },
538        }
539    }
540}