delta_base_sdk/rpc.rs
1//! # RPC Communication with the delta Base Layer
2//!
3//! This module provides a client for communicating with delta base layer through
4//! RPC (Remote Procedure Call). It enables applications to query network state, submit
5//! transactions, retrieve vault data, and subscribe to network events.
6//!
7//! ## Overview
8//!
9//! The primary interface is the [`BaseRpcClient`] which handles connection
10//! management, request serialization, and response parsing. The client provides
11//! methods for:
12//!
13//! - [Submitting transactions to the network](BaseRpcClient::submit_transaction);
14//! - Retrieving vault data ([`get_vault`](BaseRpcClient::get_vault),
15//! [`get_base_vault`](BaseRpcClient::get_base_vault),
16//! [`get_vaults`](BaseRpcClient::get_vaults));
17//! - [Checking transaction status](BaseRpcClient::get_transaction_status); or
18//! - [Subscribing to network events](BaseRpcClient::stream_base_layer_events).
19//!
20//! All RPC operations use the [`tonic`] library for gRPC communication, with automatic
21//! serialization and deserialization between protocol buffer and native Rust types.
22//!
23//! ## Usage
24//!
25//! To use the RPC client, you need a running delta base layer at some known URL.
26//!
27//! ## Example
28//!
29//! ```rust,no_run
30//! use delta_base_sdk::{
31//! core::Shard,
32//! crypto::{ed25519, Hash256, IntoSignatureEnum},
33//! rpc::{BaseRpcClient, RpcError},
34//! vaults::{Address, ReadableNativeBalance, Vault},
35//! };
36//! # use std::error::Error;
37//! # use std::str::FromStr;
38//! # use tokio_stream::StreamExt;
39//!
40//! async fn rpc_example() -> Result<(), Box<dyn Error>> {
41//! // Connect to base layer
42//! let client = BaseRpcClient::new("http://localhost:50051").await?;
43//!
44//! // Get a vault
45//! let pub_key = ed25519::PrivKey::generate().pub_key();
46//! let vault = client.get_vault(Address::new(pub_key.owner(), 1)).await?;
47//! println!("Vault balance: {}", vault.balance());
48//!
49//! // Check a transaction status
50//! // In a real app, this would be a valid signature
51//! let signature = ed25519::Signature::from_str("<MY SIGNATURE>")?
52//! .into_signature_enum(pub_key);
53//! let status = client.get_transaction_status(signature.hash_sha256()).await?;
54//! println!("Transaction status: {:?}", status);
55//!
56//! // Query current epoch
57//! let epoch = client.get_epoch().await?;
58//! println!("Current epoch: {}", epoch);
59//!
60//! // Subscribe to network events (requires tokio to run)
61//! let mut events = client.stream_base_layer_events(0).await?;
62//!
63//! // Process the first 5 events
64//! let mut count = 0;
65//! while let Some(event) = events.next().await {
66//! if let Ok(event) = event {
67//! println!("Received event: {:?}", event);
68//! count += 1;
69//! if count >= 5 {
70//! break;
71//! }
72//! }
73//! }
74//! Ok(())
75//! }
76//! ```
77
78use crate::{
79 core::Shard,
80 crypto::HashDigest,
81 events::BaseLayerEvent,
82 sdl::StateDiffList,
83 transactions::{
84 types::TransactionType,
85 SignedTransaction,
86 TransactionStatus,
87 TypeData,
88 },
89};
90use http::{
91 uri::{
92 self,
93 InvalidUri,
94 Scheme,
95 },
96 Uri,
97};
98use primitives::{
99 domain_agreement::DomainAgreement,
100 type_aliases::{
101 Epoch,
102 TxId,
103 },
104 vault::{
105 base::Vault as BaseVault,
106 sharded::Vault as ShardedVault,
107 Address,
108 OwnerId,
109 },
110};
111use proto_types::{
112 error::HashSnafu,
113 sdl::SdlStatus,
114 services::{
115 GetBaseVaultRequest,
116 GetDomainAgreementRequest,
117 GetDomainOwnerIdsRequest,
118 GetEpochRequest,
119 GetEpochResponse,
120 GetSdlDataRequest,
121 GetShardedVaultsRequest,
122 GetTransactionStatusRequest,
123 StreamBaseLayerEventRequest,
124 ValidatorServiceClient,
125 },
126};
127use snafu::{
128 ResultExt,
129 Snafu,
130};
131use std::collections::HashMap;
132use tokio_stream::{
133 Stream,
134 StreamExt,
135};
136use tonic::{
137 transport::Channel,
138 Request,
139};
140
141/// Maximal size of gRPC messages that the client can decode, and thus receive (10 MiB).
142const MAX_DECODING_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
143
144/// Errors that can occur when using the [`BaseRpcClient`]
145#[derive(Debug, Snafu)]
146pub enum RpcError {
147 /// Failed to establish a connection to base layer
148 ///
149 /// This error occurs when the client cannot connect to the specified
150 /// base layer URL, typically due to network issues or because
151 /// the node is not running.
152 #[snafu(display("Could not connect: {source}"))]
153 Connection {
154 /// The underlying transport error
155 source: tonic::transport::Error,
156 },
157
158 /// The RPC request was rejected by base layer
159 ///
160 /// This error occurs when the request is received by the base layer
161 /// but cannot be processed, such as when requesting a non-existent
162 /// vault or submitting an invalid transaction.
163 #[snafu(display("Request failed with status: {status}"))]
164 Request {
165 /// The status returned by the base layer
166 #[snafu(source(from(tonic::Status, Box::new)))]
167 status: Box<tonic::Status>,
168 },
169
170 /// Failed to convert between protocol buffer and native format
171 ///
172 /// This error occurs when there's a problem converting between the
173 /// wire format (protocol buffers) and the native Rust types, typically
174 /// due to missing or malformed data.
175 #[snafu(display("Could not convert type: {source}"))]
176 IntoProto {
177 /// The underlying conversion error
178 source: proto_types::error::ProtoError,
179 },
180
181 /// Failed to parse base layer URL
182 ///
183 /// This error occurs when the URL provided to connect to the base layer is
184 /// malformed or cannot be parsed.
185 #[snafu(display("Could not parse URL: {source}"))]
186 InvalidUrl {
187 /// The underlying HTTP error
188 source: http::Error,
189 },
190}
191
192impl RpcError {
193 /// Returns true if the underlying error is a gRPC not found status.
194 pub fn is_not_found(&self) -> bool {
195 matches!(self, Self::Request { status } if status.code() == tonic::Code::NotFound)
196 }
197
198 /// Returns true if the underlying error is a gRPC failed precondition status.
199 pub fn is_failed_precondition(&self) -> bool {
200 matches!(self, Self::Request { status } if status.code() == tonic::Code::FailedPrecondition)
201 }
202}
203
204/// Client for communicating with the delta base layer through RPC
205///
206/// [BaseRpcClient] provides a high-level interface for interacting with
207/// delta base layers. It handles connection management, request formatting,
208/// and response parsing for all base layer operations.
209///
210/// # Example
211///
212/// ```rust,no_run
213/// use delta_base_sdk::rpc::BaseRpcClient;
214///
215/// async fn connect() -> Result<(), Box<dyn std::error::Error>> {
216/// // Connect to local base layer provider
217/// let client = BaseRpcClient::new("http://localhost:50051").await?;
218///
219/// // Connect to a remote node with HTTPS
220/// let secure_client = BaseRpcClient::new("https://baselayer.example.com").await?;
221///
222/// // Connect with just the host (HTTPS will be added automatically)
223/// let auto_scheme_client = BaseRpcClient::new("baselayer.example.com").await?;
224///
225/// Ok(())
226/// }
227/// ```
228#[derive(Debug, Clone)]
229pub struct BaseRpcClient {
230 // The inner client is cloned in all calls but this is cheap and avoids mutable self refs:
231 // https://docs.rs/tonic/latest/tonic/transport/struct.Channel.html#multiplexing-requests
232 inner: ValidatorServiceClient<Channel>,
233}
234
235impl BaseRpcClient {
236 /// Creates a new client by connecting to the specified base layer provider URL
237 ///
238 /// This method establishes a connection to delta base layer at the specified
239 /// URL. If the URL does not include a scheme (http:// or https://), HTTPS will be
240 /// used automatically.
241 ///
242 /// # Parameters
243 ///
244 /// * `url` - URL of base layer provider to connect to, either as a string or a URI
245 ///
246 /// # Errors
247 ///
248 /// Returns [RpcError] if:
249 /// - The URL cannot be parsed; or
250 /// - The connection cannot be established
251 pub async fn new(url: impl TryInto<Uri, Error = InvalidUri> + Send) -> Result<Self, RpcError> {
252 let uri = add_missing_scheme(url)?;
253 let inner = ValidatorServiceClient::connect(uri.to_string())
254 .await
255 .context(ConnectionSnafu)?
256 .max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE);
257 Ok(Self { inner })
258 }
259
260 /// Submits a signed transaction to the base layer network
261 ///
262 /// This method sends a transaction to base layer for processing.
263 /// The transaction must be properly signed with a valid signature from
264 /// the transaction signer's private key.
265 ///
266 /// # Parameters
267 ///
268 /// * `tx` - Signed transaction to submit
269 ///
270 /// # Errors
271 ///
272 /// Returns [RpcError] if:
273 /// - The connection to the base layer provider fails;
274 /// - The base layer provider rejects the transaction; or
275 /// - There's a protocol conversion error.
276 pub async fn submit_transaction<T>(&self, tx: SignedTransaction<T>) -> Result<(), RpcError>
277 where
278 T: TransactionType + Into<TypeData> + Send,
279 {
280 self.inner
281 .clone()
282 .submit_transaction(Request::new(tx.into()))
283 .await
284 .context(RequestSnafu)?;
285 Ok(())
286 }
287
288 /// Gets the base vault for an owner in the base layer shard
289 ///
290 /// This is a convenience method that calls [`get_vault`](BaseRpcClient::get_vault)
291 /// with the base layer shard (0).
292 ///
293 /// # Parameters
294 ///
295 /// * `owner` - The ID of the vault owner
296 ///
297 /// # Errors
298 ///
299 /// Returns [RpcError] if:
300 /// - The vault doesn't exist;
301 /// - The connection to the base layer provider fails; or
302 /// - There's a protocol conversion error.
303 pub async fn get_base_vault(&self, owner: OwnerId) -> Result<BaseVault, RpcError> {
304 let proto_vault = self
305 .inner
306 .clone()
307 .get_base_vault(GetBaseVaultRequest {
308 owner: owner.into(),
309 })
310 .await
311 .context(RequestSnafu)?
312 .into_inner();
313
314 proto_vault.try_into().context(IntoProtoSnafu)
315 }
316
317 /// Retrieves a domain vault data for a specific address
318 ///
319 /// # Parameters
320 ///
321 /// * `address` - of the vault to fetch
322 ///
323 /// # Errors
324 ///
325 /// Returns [RpcError] if:
326 /// - The vault under the given address doesn't exist;
327 /// - The connection to the base layer provider fails; or
328 /// - There's a protocol conversion error.
329 ///
330 /// # Example
331 ///
332 /// ```rust,no_run
333 /// use delta_base_sdk::{
334 /// crypto::ed25519,
335 /// rpc::BaseRpcClient,
336 /// vaults::{Address, ReadableNativeBalance},
337 /// };
338 ///
339 /// async fn get_vault_example(client: &BaseRpcClient) -> Result<(), Box<dyn std::error::Error>> {
340 /// let address = Address::new(ed25519::PubKey::generate().owner(), 1);
341 /// let vault = client.get_vault(address).await?;
342 /// println!("Vault data: {:?}", vault);
343 /// println!("Vault balance: {}", vault.balance());
344 /// Ok(())
345 /// }
346 /// ```
347 pub async fn get_vault(&self, address: Address) -> Result<ShardedVault, RpcError> {
348 let mut vaults = self.get_vaults([address]).await?;
349 vaults.remove(&address).ok_or_else(|| RpcError::Request {
350 status: Box::new(tonic::Status::not_found("Vault not found")),
351 })
352 }
353
354 /// Retrieves multiple sharded vaults by address
355 ///
356 /// This method fetches multiple vaults in a single request, up to a maximum of 100.
357 ///
358 /// # Parameters
359 ///
360 /// * `addresses` - An iterator of addresses to retrieve
361 ///
362 /// # Errors
363 ///
364 /// Returns [RpcError] if:
365 /// - Any of the vaults don't exist;
366 /// - The connection to the base layer provider fails; or
367 /// - There's a protocol conversion error.
368 pub async fn get_vaults(
369 &self,
370 addresses: impl IntoIterator<Item = Address>,
371 ) -> Result<HashMap<Address, ShardedVault>, RpcError> {
372 // Collect the iterator first since we'll need to consume it multiple times
373 let addresses: Vec<Address> = addresses.into_iter().collect();
374 if addresses.is_empty() {
375 return Ok(HashMap::new())
376 }
377
378 let addresses_proto: Vec<_> = addresses.iter().map(|address| (*address).into()).collect();
379
380 // Fetch vaults from server
381 let vaults = self
382 .inner
383 .clone()
384 .get_sharded_vaults(GetShardedVaultsRequest {
385 addresses: addresses_proto,
386 })
387 .await
388 .context(RequestSnafu)?
389 .into_inner()
390 .vaults;
391
392 // Reconstruct addresses matching server response order
393 addresses
394 .into_iter()
395 .zip(vaults)
396 .map(|(address, vault_proto)| {
397 vault_proto
398 .try_into()
399 .context(IntoProtoSnafu)
400 .map(|vault| (address, vault))
401 })
402 .collect()
403 }
404
405 /// Returns all the [OwnerId] that have a vault registered for the given shard on the base layer
406 pub async fn get_domain_owner_ids(&self, shard: Shard) -> Result<Vec<OwnerId>, RpcError> {
407 let response = self
408 .inner
409 .clone()
410 .get_domain_owner_ids(GetDomainOwnerIdsRequest { shard })
411 .await
412 .context(RequestSnafu)?
413 .into_inner();
414 response
415 .owner_ids
416 .into_iter()
417 .map(TryInto::try_into)
418 .collect::<Result<Vec<OwnerId>, _>>()
419 .context(HashSnafu)
420 .context(IntoProtoSnafu)
421 }
422
423 /// Gets the Domain Agreement for the provided shard.
424 ///
425 /// # Parameters
426 /// * `shard` - the shard to query the agreement for
427 ///
428 /// # Errors
429 ///
430 /// Returns [RpcError] if:
431 /// - There's no active agreement;
432 /// - The connection to the base layer RPC fails; or
433 /// - There's a protocol conversion error.
434 pub async fn get_domain_agreement(&self, shard: Shard) -> Result<DomainAgreement, RpcError> {
435 self.inner
436 .clone()
437 .get_domain_agreement(GetDomainAgreementRequest { shard })
438 .await
439 .context(RequestSnafu)?
440 .into_inner()
441 .try_into()
442 .context(IntoProtoSnafu)
443 }
444
445 /// Retrieves the current status of a transaction by its signature
446 ///
447 /// This method allows tracking the progress of a transaction through the
448 /// delta network. After submitting a transaction, you can use this method
449 /// to check if it has been processed, confirmed, or rejected.
450 ///
451 /// # Parameters
452 ///
453 /// * `tx_id` - [TxId] of the transaction to query
454 ///
455 /// # Errors
456 ///
457 /// Returns [RpcError] if:
458 /// - The connection to the base layer provider fails; or
459 /// - The signature is invalid or unknown to the network.
460 ///
461 /// # Note
462 ///
463 /// Transaction signatures are unique identifiers generated when signing a
464 /// transaction. Retain the signature after submitting a transaction if you
465 /// want to check its status later.
466 pub async fn get_transaction_status(&self, tx_id: TxId) -> Result<TransactionStatus, RpcError> {
467 let res = self
468 .inner
469 .clone()
470 .get_transaction_status(GetTransactionStatusRequest {
471 tx_id: tx_id.into(),
472 })
473 .await
474 .context(RequestSnafu)?;
475 res.into_inner().try_into().context(IntoProtoSnafu)
476 }
477
478 /// Retrieves the current epoch in delta
479 ///
480 /// This method returns the current epoch number. Epochs are protocol-defined
481 /// periods in the delta protocol during which the base layer provider set
482 /// remains constant.
483 ///
484 /// # Errors
485 ///
486 /// Returns [RpcError] if:
487 /// - The connection to the base layer provider fails
488 /// - The base layer provider cannot provide epoch information
489 ///
490 /// # Example
491 ///
492 /// ```rust,no_run
493 /// use delta_base_sdk::rpc::BaseRpcClient;
494 ///
495 /// async fn get_network_info() -> Result<(), Box<dyn std::error::Error>> {
496 /// let client = BaseRpcClient::new("http://localhost:50051").await?;
497 ///
498 /// // Get information about the current epoch
499 /// let epoch = client.get_epoch().await?;
500 ///
501 /// println!("Current epoch: {}", epoch);
502 ///
503 /// Ok(())
504 /// }
505 /// ```
506 pub async fn get_epoch(&self) -> Result<Epoch, RpcError> {
507 let res: GetEpochResponse = self
508 .inner
509 .clone()
510 .get_epoch(GetEpochRequest {})
511 .await
512 .context(RequestSnafu)?
513 .into_inner();
514 Ok(res.epoch)
515 }
516
517 /// Retrieves a [State Diff List (SDL)](StateDiffList) by its hash identifier
518 ///
519 /// A State Diff List (SDL) is a batch of state changes in the delta protocol.
520 /// This method allows retrieving the complete contents of an SDL by its hash,
521 /// enabling applications to inspect state changes or verify transaction results.
522 ///
523 /// # Parameters
524 ///
525 /// * `hash` - [Cryptographic hash](HashDigest) that uniquely identifies the SDL
526 ///
527 /// # Errors
528 ///
529 /// Returns [RpcError] if:
530 /// - The SDL with the specified hash doesn't exist;
531 /// - The connection to the base layer provider fails; or
532 /// - There's a protocol conversion error.
533 pub async fn get_sdl(&self, hash: HashDigest) -> Result<StateDiffList, RpcError> {
534 let res = self
535 .inner
536 .clone()
537 .get_sdl_data(GetSdlDataRequest {
538 sdl_id: hash.into(),
539 })
540 .await
541 .context(RequestSnafu)?;
542 res.into_inner().try_into().context(IntoProtoSnafu)
543 }
544
545 /// Retrieves the [status](SdlStatus) of a [State Diff List (SDL)](StateDiffList).
546 ///
547 /// # Parameters
548 ///
549 /// * `hash` - [Cryptographic hash](HashDigest) that uniquely identifies the SDL
550 ///
551 /// # Errors
552 ///
553 /// Returns [RpcError] if:
554 /// - The SDL with the specified hash doesn't exist;
555 /// - The connection to the base layer provider fails; or
556 /// - There's a protocol conversion error.
557 pub async fn get_sdl_status(&self, hash: HashDigest) -> Result<SdlStatus, RpcError> {
558 let res = self
559 .inner
560 .clone()
561 .get_sdl_status(GetSdlDataRequest {
562 sdl_id: hash.into(),
563 })
564 .await
565 .context(RequestSnafu)?;
566
567 res.into_inner().try_into().context(IntoProtoSnafu)
568 }
569
570 /// Subscribes to a stream of real-time events from the delta network
571 ///
572 /// This method establishes a streaming connection to receive events from the
573 /// base layer as they occur. Events include epoch transitions, vault migrations,
574 /// and other network state changes. The events are filtered by shard.
575 ///
576 /// # Parameters
577 ///
578 /// * `shard` - [Shard] number to receive events for
579 ///
580 /// # Errors
581 ///
582 /// Returns [RpcError] if:
583 /// - The connection to the base layer provider fails;
584 /// - The streaming request is rejected; or
585 /// - Individual stream items may contain errors if there are issues during streaming.
586 ///
587 /// # Example
588 ///
589 /// ```rust,no_run
590 /// use delta_base_sdk::{
591 /// events::BaseLayerEvent,
592 /// rpc::BaseRpcClient,
593 /// };
594 /// use tokio_stream::StreamExt;
595 ///
596 /// async fn monitor_events() -> Result<(), Box<dyn std::error::Error>> {
597 /// let client = BaseRpcClient::new("http://localhost:50051").await?;
598 ///
599 /// // Subscribe to base layer events for shard 0 (the base layer shard)
600 /// let mut event_stream = client.stream_base_layer_events(0).await?;
601 ///
602 /// println!("Listening for events...");
603 ///
604 /// // Process events as they arrive
605 /// while let Some(event_result) = event_stream.next().await {
606 /// match event_result {
607 /// Ok(event) => {
608 /// match event {
609 /// BaseLayerEvent::NewEpoch(epoch) => {
610 /// println!("New epoch started: {}", epoch);
611 /// },
612 /// BaseLayerEvent::SdlUpdate(update) => {
613 /// println!("SDL update received: {:?}", update.id);
614 /// },
615 /// BaseLayerEvent::VaultEmigrated(address) => {
616 /// println!("Vault emigrated: {address:?}");
617 /// },
618 /// BaseLayerEvent::VaultImmigrated(address) => {
619 /// println!("Vault immigrated: {address:?}");
620 /// },
621 /// }
622 /// },
623 /// Err(e) => {
624 /// eprintln!("Error in event stream: {e}");
625 /// // Decide whether to break or continue based on the error
626 /// }
627 /// }
628 /// }
629 ///
630 /// Ok(())
631 /// }
632 /// ```
633 ///
634 /// # Note
635 ///
636 /// The returned stream implements the `Stream` trait from the `tokio_stream` crate.
637 /// You need to bring the `StreamExt` trait into scope to use methods like `next()`.
638 ///
639 /// This is a long-lived connection that will continue to deliver events until
640 /// the stream is dropped or the connection is closed. It's suitable for building
641 /// reactive applications that need to respond to network events in real-time.
642 pub async fn stream_base_layer_events(
643 &self,
644 shard: Shard,
645 ) -> Result<impl Stream<Item = Result<BaseLayerEvent, RpcError>> + Send, RpcError> {
646 let res = self
647 .inner
648 .clone()
649 .stream_base_layer_events(StreamBaseLayerEventRequest { shard })
650 .await
651 .context(RequestSnafu)?;
652
653 let stream = res.into_inner().map(|r| {
654 r.context(RequestSnafu)
655 .and_then(|u| u.try_into().context(IntoProtoSnafu))
656 });
657
658 Ok(stream)
659 }
660}
661
662/// The default URI scheme to use when none is provided
663const DEFAULT_SCHEME: Scheme = Scheme::HTTPS;
664
665/// Adds the HTTPS scheme to a URL if it doesn't already have a scheme
666///
667/// This utility function ensures that all URLs have a scheme, adding HTTPS
668/// as the default if none is specified. This allows users to provide just
669/// the host and port (e.g., "localhost:50051") without having to specify
670/// the scheme.
671///
672/// # Parameters
673///
674/// * `url` - URL to process, which may or may not have a scheme
675///
676/// # Errors
677///
678/// Returns [RpcError] if:
679/// - The URL cannot be parsed as a valid URI
680/// - The modified URI cannot be built
681#[allow(clippy::result_large_err)]
682fn add_missing_scheme(url: impl TryInto<Uri, Error = InvalidUri>) -> Result<Uri, RpcError> {
683 let uri: Uri = url
684 .try_into()
685 .map_err(|e| e.into())
686 .context(InvalidUrlSnafu)?;
687
688 if uri.scheme().is_some() {
689 Ok(uri)
690 } else {
691 let path = uri
692 .path_and_query()
693 .map(|p| p.as_str())
694 .unwrap_or("/")
695 .to_string();
696 Into::<uri::Builder>::into(uri)
697 .scheme(DEFAULT_SCHEME)
698 .path_and_query(path)
699 .build()
700 .context(InvalidUrlSnafu)
701 }
702}
703
704#[cfg(test)]
705mod test {
706 use super::*;
707
708 #[test]
709 fn test_add_missing_scheme() {
710 // leaves existing scheme
711 assert_eq!(
712 "http://localhost:5005/",
713 add_missing_scheme("http://localhost:5005")
714 .unwrap()
715 .to_string()
716 );
717 assert_eq!(
718 "http://localhost:5005/path/yes",
719 add_missing_scheme("http://localhost:5005/path/yes")
720 .unwrap()
721 .to_string()
722 );
723 assert_eq!(
724 "yolo://net.delta.net/",
725 add_missing_scheme("yolo://net.delta.net")
726 .unwrap()
727 .to_string()
728 );
729
730 // adds scheme if missing
731 assert_eq!(
732 "https://net.delta.net/",
733 add_missing_scheme("net.delta.net").unwrap().to_string()
734 );
735 assert_eq!(
736 "https://localhost:12345/",
737 add_missing_scheme("localhost:12345").unwrap().to_string()
738 );
739 }
740}