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    pub owner: AccountOwner,
140    pub recipient: Account,
141    pub amount: Amount,
142}
143
144/// Claim operation metadata.
145#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
146pub struct ClaimOperationMetadata {
147    pub owner: AccountOwner,
148    pub target_id: ChainId,
149    pub recipient: Account,
150    pub amount: Amount,
151}
152
153/// Open chain operation metadata.
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
155pub struct OpenChainOperationMetadata {
156    pub balance: Amount,
157    pub ownership: ChainOwnershipMetadata,
158    pub application_permissions: ApplicationPermissionsMetadata,
159}
160
161/// Change ownership operation metadata.
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
163pub struct ChangeOwnershipOperationMetadata {
164    pub super_owners: Vec<AccountOwner>,
165    pub owners: Vec<OwnerWithWeight>,
166    pub first_leader: Option<AccountOwner>,
167    pub multi_leader_rounds: i32,
168    pub open_multi_leader_rounds: bool,
169    pub timeout_config: TimeoutConfigMetadata,
170}
171
172/// Owner with weight metadata.
173#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
174pub struct OwnerWithWeight {
175    pub owner: AccountOwner,
176    pub weight: String, // Using String to represent u64 safely in GraphQL
177}
178
179/// Change application permissions operation metadata.
180#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
181pub struct ChangeApplicationPermissionsMetadata {
182    pub permissions: ApplicationPermissionsMetadata,
183}
184
185/// Admin operation metadata.
186#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
187pub struct AdminOperationMetadata {
188    pub admin_operation_type: String,
189    pub epoch: Option<i32>,
190    pub blob_hash: Option<CryptoHash>,
191}
192
193/// Create application operation metadata.
194#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
195pub struct CreateApplicationOperationMetadata {
196    pub module_id: String,
197    pub parameters_hex: String,
198    pub instantiation_argument_hex: String,
199    pub required_application_ids: Vec<ApplicationId>,
200}
201
202/// Publish data blob operation metadata.
203#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
204pub struct PublishDataBlobMetadata {
205    pub blob_hash: CryptoHash,
206}
207
208/// Verify blob operation metadata.
209#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
210pub struct VerifyBlobMetadata {
211    pub blob_id: String,
212}
213
214/// Publish module operation metadata.
215#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
216pub struct PublishModuleMetadata {
217    pub module_id: String,
218}
219
220/// Update stream metadata.
221#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
222pub struct UpdateStreamMetadata {
223    pub application_id: String,
224    pub chain_id: ChainId,
225    pub stream_id: String,
226    pub next_index: i32,
227}
228
229/// Structured representation of a system message for GraphQL.
230#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
231pub struct SystemMessageMetadata {
232    /// The type of system message
233    pub system_message_type: String,
234    /// Credit message details
235    pub credit: Option<CreditMessageMetadata>,
236    /// Withdraw message details
237    pub withdraw: Option<WithdrawMessageMetadata>,
238    /// CheckpointAck message details
239    pub checkpoint_ack: Option<CheckpointAckMessageMetadata>,
240}
241
242/// Credit message metadata.
243#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
244pub struct CreditMessageMetadata {
245    pub target: AccountOwner,
246    pub amount: Amount,
247    pub source: AccountOwner,
248}
249
250/// Withdraw message metadata.
251#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
252pub struct WithdrawMessageMetadata {
253    pub owner: AccountOwner,
254    pub amount: Amount,
255    pub recipient: Account,
256}
257
258/// CheckpointAck message metadata.
259#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
260pub struct CheckpointAckMessageMetadata {
261    pub latest_received_cursor: Cursor,
262}
263
264/// Structured representation of a message for GraphQL.
265#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
266pub struct MessageMetadata {
267    /// The type of message: "System" or "User"
268    pub message_type: String,
269    /// For user messages, the application ID
270    pub application_id: Option<ApplicationId>,
271    /// For user messages, the serialized bytes (as a hex string for GraphQL)
272    pub user_bytes_hex: Option<String>,
273    /// For system messages, structured representation
274    pub system_message: Option<SystemMessageMetadata>,
275}
276
277impl From<&SystemOperation> for SystemOperationMetadata {
278    fn from(sys_op: &SystemOperation) -> Self {
279        match sys_op {
280            SystemOperation::Transfer {
281                owner,
282                recipient,
283                amount,
284            } => SystemOperationMetadata {
285                transfer: Some(TransferOperationMetadata {
286                    owner: *owner,
287                    recipient: *recipient,
288                    amount: *amount,
289                }),
290                ..SystemOperationMetadata::new("Transfer")
291            },
292            SystemOperation::Claim {
293                owner,
294                target_id,
295                recipient,
296                amount,
297            } => SystemOperationMetadata {
298                claim: Some(ClaimOperationMetadata {
299                    owner: *owner,
300                    target_id: *target_id,
301                    recipient: *recipient,
302                    amount: *amount,
303                }),
304                ..SystemOperationMetadata::new("Claim")
305            },
306            SystemOperation::OpenChain(config) => SystemOperationMetadata {
307                open_chain: Some(OpenChainOperationMetadata {
308                    balance: config.balance,
309                    ownership: ChainOwnershipMetadata::from(&config.ownership),
310                    application_permissions: ApplicationPermissionsMetadata::from(
311                        &config.application_permissions,
312                    ),
313                }),
314                ..SystemOperationMetadata::new("OpenChain")
315            },
316            SystemOperation::CloseChain => SystemOperationMetadata::new("CloseChain"),
317            SystemOperation::ChangeOwnership {
318                super_owners,
319                owners,
320                first_leader,
321                multi_leader_rounds,
322                open_multi_leader_rounds,
323                timeout_config,
324            } => SystemOperationMetadata {
325                change_ownership: Some(ChangeOwnershipOperationMetadata {
326                    super_owners: super_owners.clone(),
327                    owners: owners
328                        .iter()
329                        .map(|(owner, weight)| OwnerWithWeight {
330                            owner: *owner,
331                            weight: weight.to_string(),
332                        })
333                        .collect(),
334                    first_leader: *first_leader,
335                    multi_leader_rounds: *multi_leader_rounds as i32,
336                    open_multi_leader_rounds: *open_multi_leader_rounds,
337                    timeout_config: TimeoutConfigMetadata::from(timeout_config),
338                }),
339                ..SystemOperationMetadata::new("ChangeOwnership")
340            },
341            SystemOperation::ChangeApplicationPermissions(permissions) => SystemOperationMetadata {
342                change_application_permissions: Some(ChangeApplicationPermissionsMetadata {
343                    permissions: ApplicationPermissionsMetadata::from(permissions),
344                }),
345                ..SystemOperationMetadata::new("ChangeApplicationPermissions")
346            },
347            SystemOperation::Admin(admin_op) => SystemOperationMetadata {
348                admin: Some(AdminOperationMetadata::from(admin_op)),
349                ..SystemOperationMetadata::new("Admin")
350            },
351            SystemOperation::CreateApplication {
352                module_id,
353                parameters,
354                instantiation_argument,
355                required_application_ids,
356            } => SystemOperationMetadata {
357                create_application: Some(CreateApplicationOperationMetadata {
358                    module_id: module_id.to_string(),
359                    parameters_hex: hex::encode(parameters),
360                    instantiation_argument_hex: hex::encode(instantiation_argument),
361                    required_application_ids: required_application_ids.clone(),
362                }),
363                ..SystemOperationMetadata::new("CreateApplication")
364            },
365            SystemOperation::PublishDataBlob { blob_hash } => SystemOperationMetadata {
366                publish_data_blob: Some(PublishDataBlobMetadata {
367                    blob_hash: *blob_hash,
368                }),
369                ..SystemOperationMetadata::new("PublishDataBlob")
370            },
371            SystemOperation::VerifyBlob { blob_id } => SystemOperationMetadata {
372                verify_blob: Some(VerifyBlobMetadata {
373                    blob_id: blob_id.to_string(),
374                }),
375                ..SystemOperationMetadata::new("VerifyBlob")
376            },
377            SystemOperation::PublishModule { module_id } => SystemOperationMetadata {
378                publish_module: Some(PublishModuleMetadata {
379                    module_id: module_id.to_string(),
380                }),
381                ..SystemOperationMetadata::new("PublishModule")
382            },
383            SystemOperation::ProcessNewEpoch(epoch) => SystemOperationMetadata {
384                epoch: Some(epoch.0 as i32),
385                ..SystemOperationMetadata::new("ProcessNewEpoch")
386            },
387            SystemOperation::UpdateStream {
388                application_id,
389                chain_id,
390                stream_id,
391                next_index,
392            } => SystemOperationMetadata {
393                update_stream: Some(UpdateStreamMetadata {
394                    application_id: application_id.to_string(),
395                    chain_id: *chain_id,
396                    stream_id: stream_id.to_string(),
397                    next_index: *next_index as i32,
398                }),
399                ..SystemOperationMetadata::new("UpdateStream")
400            },
401            SystemOperation::Checkpoint => SystemOperationMetadata::new("Checkpoint"),
402        }
403    }
404}
405
406impl From<&AdminOperation> for AdminOperationMetadata {
407    fn from(admin_op: &AdminOperation) -> Self {
408        match admin_op {
409            AdminOperation::PublishCommitteeBlob { blob_hash } => AdminOperationMetadata {
410                admin_operation_type: "PublishCommitteeBlob".to_string(),
411                epoch: None,
412                blob_hash: Some(*blob_hash),
413            },
414            AdminOperation::CreateCommittee { epoch, blob_hash } => AdminOperationMetadata {
415                admin_operation_type: "CreateCommittee".to_string(),
416                epoch: Some(epoch.0 as i32),
417                blob_hash: Some(*blob_hash),
418            },
419            AdminOperation::RemoveCommittee { epoch } => AdminOperationMetadata {
420                admin_operation_type: "RemoveCommittee".to_string(),
421                epoch: Some(epoch.0 as i32),
422                blob_hash: None,
423            },
424        }
425    }
426}
427
428impl From<&Message> for MessageMetadata {
429    fn from(message: &Message) -> Self {
430        match message {
431            Message::System(sys_msg) => MessageMetadata {
432                message_type: "System".to_string(),
433                application_id: None,
434                user_bytes_hex: None,
435                system_message: Some(SystemMessageMetadata::from(sys_msg)),
436            },
437            Message::User {
438                application_id,
439                bytes,
440            } => MessageMetadata {
441                message_type: "User".to_string(),
442                application_id: Some(*application_id),
443                user_bytes_hex: Some(hex::encode(bytes)),
444                system_message: None,
445            },
446        }
447    }
448}
449
450impl From<&SystemMessage> for SystemMessageMetadata {
451    fn from(sys_msg: &SystemMessage) -> Self {
452        match sys_msg {
453            SystemMessage::Credit {
454                target,
455                amount,
456                source,
457            } => SystemMessageMetadata {
458                system_message_type: "Credit".to_string(),
459                credit: Some(CreditMessageMetadata {
460                    target: *target,
461                    amount: *amount,
462                    source: *source,
463                }),
464                withdraw: None,
465                checkpoint_ack: None,
466            },
467            SystemMessage::Withdraw {
468                owner,
469                amount,
470                recipient,
471            } => SystemMessageMetadata {
472                system_message_type: "Withdraw".to_string(),
473                credit: None,
474                withdraw: Some(WithdrawMessageMetadata {
475                    owner: *owner,
476                    amount: *amount,
477                    recipient: *recipient,
478                }),
479                checkpoint_ack: None,
480            },
481            SystemMessage::CheckpointAck {
482                latest_received_cursor,
483            } => SystemMessageMetadata {
484                system_message_type: "CheckpointAck".to_string(),
485                credit: None,
486                withdraw: None,
487                checkpoint_ack: Some(CheckpointAckMessageMetadata {
488                    latest_received_cursor: *latest_received_cursor,
489                }),
490            },
491        }
492    }
493}