linera_service_graphql_client/
service.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use graphql_client::GraphQLQuery;
5use linera_base::{
6    crypto::CryptoHash,
7    data_types::{Amount, Blob, BlockHeight, ChainDescription, OracleResponse, Round, Timestamp},
8    identifiers::{Account, AccountOwner, BlobId, ChainId, GenericApplicationId, StreamName},
9};
10use thiserror::Error;
11
12pub type JSONObject = serde_json::Value;
13
14#[cfg(target_arch = "wasm32")]
15mod types {
16    use linera_base::data_types::Round;
17    use serde::{Deserialize, Serialize};
18    use serde_json::Value;
19
20    use super::{BlockHeight, ChainId, CryptoHash};
21
22    pub type ChainManager = Value;
23    pub type ChainOwnership = Value;
24    pub type Epoch = Value;
25    pub type MessageBundle = Value;
26    pub type MessageKind = Value;
27    pub type Message = Value;
28    pub type MessageAction = Value;
29    pub type Operation = Value;
30    pub type Origin = Value;
31    pub type ApplicationDescription = Value;
32    pub type OperationResult = Value;
33
34    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
35    pub struct Notification {
36        pub chain_id: ChainId,
37        pub reason: Reason,
38    }
39
40    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
41    #[expect(clippy::enum_variant_names)]
42    pub enum Reason {
43        NewBlock {
44            height: BlockHeight,
45            hash: CryptoHash,
46        },
47        NewIncomingBundle {
48            origin: Origin,
49            height: BlockHeight,
50        },
51        NewRound {
52            height: BlockHeight,
53            round: Round,
54        },
55    }
56}
57
58#[cfg(not(target_arch = "wasm32"))]
59mod types {
60    pub use linera_base::{
61        data_types::{ApplicationDescription, Epoch},
62        ownership::ChainOwnership,
63    };
64    pub use linera_chain::{
65        data_types::{IncomingBundle, MessageAction, MessageBundle, OperationResult, Transaction},
66        manager::ChainManager,
67    };
68    pub use linera_core::worker::{Notification, Reason};
69    pub use linera_execution::{Message, MessageKind, Operation, SystemOperation};
70}
71
72pub use types::*;
73pub type ApplicationId = String;
74
75#[derive(GraphQLQuery)]
76#[graphql(
77    schema_path = "gql/service_schema.graphql",
78    query_path = "gql/service_requests.graphql",
79    response_derives = "Debug, Serialize, Clone"
80)]
81pub struct Chain;
82
83#[derive(GraphQLQuery)]
84#[graphql(
85    schema_path = "gql/service_schema.graphql",
86    query_path = "gql/service_requests.graphql",
87    response_derives = "Debug, Serialize, Clone"
88)]
89pub struct Chains;
90
91#[derive(GraphQLQuery)]
92#[graphql(
93    schema_path = "gql/service_schema.graphql",
94    query_path = "gql/service_requests.graphql",
95    response_derives = "Debug, Serialize, Clone, PartialEq"
96)]
97pub struct Applications;
98
99#[derive(GraphQLQuery)]
100#[graphql(
101    schema_path = "gql/service_schema.graphql",
102    query_path = "gql/service_requests.graphql",
103    response_derives = "Debug, Serialize, Clone, PartialEq"
104)]
105pub struct Blocks;
106
107#[derive(GraphQLQuery)]
108#[graphql(
109    schema_path = "gql/service_schema.graphql",
110    query_path = "gql/service_requests.graphql",
111    response_derives = "Debug, Serialize, Clone, PartialEq"
112)]
113pub struct Block;
114
115#[derive(GraphQLQuery)]
116#[graphql(
117    schema_path = "gql/service_schema.graphql",
118    query_path = "gql/service_requests.graphql",
119    response_derives = "Debug, Serialize, Clone, PartialEq"
120)]
121pub struct Notifications;
122
123#[derive(GraphQLQuery)]
124#[graphql(
125    schema_path = "gql/service_schema.graphql",
126    query_path = "gql/service_requests.graphql",
127    response_derives = "Debug, Serialize, Clone"
128)]
129pub struct Transfer;
130
131#[derive(Error, Debug)]
132pub enum ConversionError {
133    #[error(transparent)]
134    Serde(#[from] serde_json::Error),
135    #[error("Unexpected certificate type: {0}")]
136    UnexpectedCertificateType(String),
137}
138
139#[cfg(not(target_arch = "wasm32"))]
140mod from {
141    use linera_base::{data_types::Event, identifiers::StreamId};
142    use linera_chain::{
143        block::{Block, BlockBody, BlockHeader},
144        types::ConfirmedBlock,
145    };
146    use linera_execution::OutgoingMessage;
147
148    use super::*;
149
150    /// Convert GraphQL transaction metadata to a Transaction object
151    fn convert_transaction_metadata(
152        metadata: block::BlockBlockBlockBodyTransactionMetadata,
153    ) -> Result<Transaction, ConversionError> {
154        match metadata.transaction_type.as_str() {
155            "ReceiveMessages" => {
156                let incoming_bundle = metadata.incoming_bundle.ok_or_else(|| {
157                    ConversionError::UnexpectedCertificateType(
158                        "Missing incoming_bundle for ReceiveMessages transaction".to_string(),
159                    )
160                })?;
161
162                let bundle = IncomingBundle {
163                    origin: incoming_bundle.origin,
164                    bundle: MessageBundle {
165                        height: incoming_bundle.bundle.height,
166                        timestamp: incoming_bundle.bundle.timestamp,
167                        certificate_hash: incoming_bundle.bundle.certificate_hash,
168                        transaction_index: incoming_bundle.bundle.transaction_index as u32,
169                        messages: incoming_bundle
170                            .bundle
171                            .messages
172                            .into_iter()
173                            .map(|msg| linera_chain::data_types::PostedMessage {
174                                authenticated_signer: msg.authenticated_signer,
175                                grant: msg.grant,
176                                refund_grant_to: msg.refund_grant_to,
177                                kind: msg.kind,
178                                index: msg.index as u32,
179                                message: msg.message,
180                            })
181                            .collect(),
182                    },
183                    action: incoming_bundle.action,
184                };
185
186                Ok(Transaction::ReceiveMessages(bundle))
187            }
188            "ExecuteOperation" => {
189                let graphql_operation = metadata.operation.ok_or_else(|| {
190                    ConversionError::UnexpectedCertificateType(
191                        "Missing operation for ExecuteOperation transaction".to_string(),
192                    )
193                })?;
194
195                let operation = match graphql_operation.operation_type.as_str() {
196                    "System" => {
197                        let bytes_hex = graphql_operation.system_bytes_hex.ok_or_else(|| {
198                            ConversionError::UnexpectedCertificateType(
199                                "Missing system_bytes_hex for System operation".to_string(),
200                            )
201                        })?;
202
203                        // Convert hex string to bytes
204                        let bytes = hex::decode(bytes_hex).map_err(|_| {
205                            ConversionError::UnexpectedCertificateType(
206                                "Invalid hex in system_bytes_hex".to_string(),
207                            )
208                        })?;
209
210                        // Deserialize the system operation from BCS bytes
211                        let system_operation: SystemOperation =
212                            linera_base::bcs::from_bytes(&bytes).map_err(|_| {
213                                ConversionError::UnexpectedCertificateType(
214                                    "Failed to deserialize system operation from BCS bytes"
215                                        .to_string(),
216                                )
217                            })?;
218
219                        Operation::System(Box::new(system_operation))
220                    }
221                    "User" => {
222                        let application_id = graphql_operation.application_id.ok_or_else(|| {
223                            ConversionError::UnexpectedCertificateType(
224                                "Missing application_id for User operation".to_string(),
225                            )
226                        })?;
227
228                        let bytes_hex = graphql_operation.user_bytes_hex.ok_or_else(|| {
229                            ConversionError::UnexpectedCertificateType(
230                                "Missing user_bytes_hex for User operation".to_string(),
231                            )
232                        })?;
233
234                        // Convert hex string to bytes
235                        let bytes = hex::decode(bytes_hex).map_err(|_| {
236                            ConversionError::UnexpectedCertificateType(
237                                "Invalid hex in user_bytes_hex".to_string(),
238                            )
239                        })?;
240
241                        Operation::User {
242                            application_id: application_id.parse().map_err(|_| {
243                                ConversionError::UnexpectedCertificateType(
244                                    "Invalid application_id format".to_string(),
245                                )
246                            })?,
247                            bytes,
248                        }
249                    }
250                    _ => {
251                        return Err(ConversionError::UnexpectedCertificateType(format!(
252                            "Unknown operation type: {}",
253                            graphql_operation.operation_type
254                        )));
255                    }
256                };
257
258                Ok(Transaction::ExecuteOperation(operation))
259            }
260            _ => Err(ConversionError::UnexpectedCertificateType(format!(
261                "Unknown transaction type: {}",
262                metadata.transaction_type
263            ))),
264        }
265    }
266
267    impl From<block::BlockBlockBlockBodyMessages> for OutgoingMessage {
268        fn from(val: block::BlockBlockBlockBodyMessages) -> Self {
269            let block::BlockBlockBlockBodyMessages {
270                destination,
271                authenticated_signer,
272                grant,
273                refund_grant_to,
274                kind,
275                message,
276            } = val;
277            OutgoingMessage {
278                destination,
279                authenticated_signer,
280                grant,
281                refund_grant_to,
282                kind,
283                message,
284            }
285        }
286    }
287
288    impl TryFrom<block::BlockBlockBlock> for Block {
289        type Error = ConversionError;
290
291        fn try_from(val: block::BlockBlockBlock) -> Result<Self, Self::Error> {
292            let block::BlockBlockBlock { header, body } = val;
293            let block::BlockBlockBlockHeader {
294                chain_id,
295                epoch,
296                height,
297                timestamp,
298                authenticated_signer,
299                previous_block_hash,
300                state_hash,
301                transactions_hash,
302                messages_hash,
303                previous_message_blocks_hash,
304                previous_event_blocks_hash,
305                oracle_responses_hash,
306                events_hash,
307                blobs_hash,
308                operation_results_hash,
309            } = header;
310            let block::BlockBlockBlockBody {
311                messages,
312                previous_message_blocks,
313                previous_event_blocks,
314                oracle_responses,
315                events,
316                blobs,
317                operation_results,
318                transaction_metadata,
319            } = body;
320
321            let block_header = BlockHeader {
322                chain_id,
323                epoch,
324                height,
325                timestamp,
326                authenticated_signer,
327                previous_block_hash,
328                state_hash,
329                transactions_hash,
330                messages_hash,
331                previous_message_blocks_hash,
332                previous_event_blocks_hash,
333                oracle_responses_hash,
334                events_hash,
335                blobs_hash,
336                operation_results_hash,
337            };
338
339            // Convert GraphQL transaction metadata to Transaction objects
340            let transactions = transaction_metadata
341                .into_iter()
342                .map(convert_transaction_metadata)
343                .collect::<Result<Vec<_>, _>>()?;
344
345            let block_body = BlockBody {
346                transactions,
347                messages: messages
348                    .into_iter()
349                    .map(|messages| messages.into_iter().map(Into::into).collect())
350                    .collect::<Vec<Vec<_>>>(),
351                previous_message_blocks: serde_json::from_value(previous_message_blocks)
352                    .map_err(ConversionError::Serde)?,
353                previous_event_blocks: serde_json::from_value(previous_event_blocks)
354                    .map_err(ConversionError::Serde)?,
355                oracle_responses: oracle_responses.into_iter().collect(),
356                events: events
357                    .into_iter()
358                    .map(|events| events.into_iter().map(Into::into).collect())
359                    .collect(),
360                blobs: blobs
361                    .into_iter()
362                    .map(|blobs| blobs.into_iter().collect())
363                    .collect(),
364                operation_results,
365            };
366
367            Ok(Block {
368                header: block_header,
369                body: block_body,
370            })
371        }
372    }
373
374    impl From<block::BlockBlockBlockBodyEvents> for Event {
375        fn from(event: block::BlockBlockBlockBodyEvents) -> Self {
376            Event {
377                stream_id: event.stream_id.into(),
378                index: event.index as u32,
379                value: event.value.into_iter().map(|byte| byte as u8).collect(),
380            }
381        }
382    }
383
384    impl From<block::BlockBlockBlockBodyEventsStreamId> for StreamId {
385        fn from(stream_id: block::BlockBlockBlockBodyEventsStreamId) -> Self {
386            StreamId {
387                application_id: stream_id.application_id,
388                stream_name: stream_id.stream_name,
389            }
390        }
391    }
392
393    impl TryFrom<block::BlockBlock> for ConfirmedBlock {
394        type Error = ConversionError;
395
396        fn try_from(val: block::BlockBlock) -> Result<Self, Self::Error> {
397            match (val.status.as_str(), val.block) {
398                ("confirmed", block) => Ok(ConfirmedBlock::new(block.try_into()?)),
399                _ => Err(ConversionError::UnexpectedCertificateType(val.status)),
400            }
401        }
402    }
403}