unc_workspaces/rpc/
query.rs

1//! This module defines a bunch of internal types used solely for querying into RPC
2//! methods to retrieve info about what's on the chain. Note that the types defined
3//! are exposed as-is for users to reference in their own functions or structs as
4//! needed. These types cannot be created outside of workspaces. To use them, refer
5//! to surface level types like [`Account`], [`Contract`] and [`Worker`].
6//!
7//! For example, to query into downloading contract state:
8//! ```
9//! use unc_workspaces::{AccountId, Network, Worker};
10//! use unc_workspaces::rpc::query::{Query, ViewState};
11//!
12//! async fn my_func(worker: &Worker<impl Network>) -> anyhow::Result<()> {
13//!     let contract_id: AccountId = "some-contract.unc".parse()?;
14//!     let query: Query<'_, ViewState> = worker.view_state(&contract_id);
15//!     let bytes = query.await?;
16//!     Ok(())
17//! }
18//! ```
19//! But most of the time, we do not need to worry about these types as they are
20//! meant to be transitory, and only exist while calling into their immediate
21//! methods. So the above example should look more like the following:
22//! ```ignore
23//! async fn my_func(worker: &Worker<impl Network>) -> anyhow::Result<()> {
24//!     let contract_id: AccountId = "some-contract.unc".parse()?;
25//!     let bytes = worker.view_state(&contract_id).await?;
26//!     Ok(())
27//! }
28//! ```
29//!
30//! [`Account`]: crate::Account
31//! [`Contract`]: crate::Contract
32//! [`Worker`]: crate::Worker
33
34use std::collections::HashMap;
35use std::fmt::{Debug, Display};
36
37use unc_account_id::AccountId;
38use unc_jsonrpc_client::methods::query::RpcQueryResponse;
39use unc_jsonrpc_client::methods::{self, RpcMethod};
40use unc_jsonrpc_primitives::types::chunks::ChunkReference;
41use unc_jsonrpc_primitives::types::query::QueryResponseKind;
42use unc_primitives::types::{BlockId, BlockReference, StoreKey};
43use unc_primitives::views::{BlockView, QueryRequest};
44use unc_token::UncToken;
45
46use crate::error::RpcErrorCode;
47use crate::operations::Function;
48use crate::result::ViewResultDetails;
49use crate::rpc::client::Client;
50use crate::rpc::{tool, BoxFuture};
51use crate::types::account::AccountDetails;
52use crate::types::{AccessKey, AccessKeyInfo, BlockHeight, Finality, PublicKey, ShardId};
53use crate::{Block, Chunk, CryptoHash, Result};
54
55/// `Query` object allows creating queries into the network of our choice. This object is
56/// usually given from making calls from other functions such as [`view_state`].
57///
58/// [`view_state`]: crate::worker::Worker::view_state
59pub struct Query<'a, T> {
60    pub(crate) method: T,
61    pub(crate) client: &'a Client,
62    pub(crate) block_ref: Option<BlockReference>,
63}
64
65impl<'a, T> Query<'a, T> {
66    pub(crate) fn new(client: &'a Client, method: T) -> Self {
67        Self {
68            method,
69            client,
70            block_ref: None,
71        }
72    }
73
74    /// Specify at which block height to query from. Note that only archival
75    /// networks will have the full history while networks like mainnet or testnet will
76    /// only have the history from 5 or less epochs ago.
77    pub fn block_height(mut self, height: BlockHeight) -> Self {
78        self.block_ref = Some(BlockId::Height(height).into());
79        self
80    }
81
82    /// Specify at which block hash to query from. Note that only archival
83    /// networks will have the full history while networks like mainnet or testnet will
84    /// only have the history from 5 or less epochs ago.
85    pub fn block_hash(mut self, hash: CryptoHash) -> Self {
86        self.block_ref = Some(BlockId::Hash(unc_primitives::hash::CryptoHash(hash.0)).into());
87        self
88    }
89}
90
91// Constrained to RpcQueryRequest, since methods like GasPrice only take block_id but not Finality.
92impl<'a, T> Query<'a, T>
93where
94    T: ProcessQuery<Method = methods::query::RpcQueryRequest>,
95{
96    /// Specify at which block [`Finality`] to query from.
97    pub fn finality(mut self, value: Finality) -> Self {
98        self.block_ref = Some(value.into());
99        self
100    }
101
102    pub(crate) fn block_reference(mut self, value: BlockReference) -> Self {
103        self.block_ref = Some(value);
104        self
105    }
106}
107
108impl<'a, T, R> std::future::IntoFuture for Query<'a, T>
109where
110    T: ProcessQuery<Output = R> + Send + Sync + 'static,
111    <T as ProcessQuery>::Method: RpcMethod + Debug + Send + Sync,
112    <<T as ProcessQuery>::Method as RpcMethod>::Response: Debug + Send + Sync,
113    <<T as ProcessQuery>::Method as RpcMethod>::Error: Debug + Display + Send + Sync,
114{
115    type Output = Result<R>;
116
117    // TODO: boxed future required due to impl Trait as type alias being unstable. So once
118    // https://github.com/rust-lang/rust/issues/63063 is resolved, we can move to that instead.
119    type IntoFuture = BoxFuture<'a, Self::Output>;
120
121    fn into_future(self) -> Self::IntoFuture {
122        Box::pin(async move {
123            let block_reference = self.block_ref.unwrap_or_else(BlockReference::latest);
124            let resp = self
125                .client
126                .query(self.method.into_request(block_reference)?)
127                .await
128                .map_err(|e| RpcErrorCode::QueryFailure.custom(e))?;
129
130            T::from_response(resp)
131        })
132    }
133}
134
135// Note: this trait is exposed publicly due to constraining with the impl offering `finality`.
136/// Trait used as a converter from WorkspaceRequest to unc-rpc request, and from unc-rpc
137/// response to a WorkspaceResult. Mostly used internally to facilitate syntax sugar for performing
138/// RPC requests with async builders.
139pub trait ProcessQuery {
140    // TODO: associated default type is unstable. So for now, will require writing
141    // the manual impls for query_request
142    /// Method for doing the internal RPC request to the network of our choosing.
143    type Method: RpcMethod;
144
145    /// Expected output after performing a query. This is mainly to convert over
146    /// the type from unc-primitives to a workspace type.
147    type Output;
148
149    /// Convert into the Request object that is required to perform the RPC request.
150    fn into_request(self, block_ref: BlockReference) -> Result<Self::Method>;
151
152    /// Convert the response from the RPC request to a type of our choosing, mainly to conform
153    /// to workspaces related types from the unc-primitives or json types from the network.
154    fn from_response(resp: <Self::Method as RpcMethod>::Response) -> Result<Self::Output>;
155}
156
157pub struct ViewFunction {
158    pub(crate) account_id: AccountId,
159    pub(crate) function: Function,
160}
161
162pub struct ViewCode {
163    pub(crate) account_id: AccountId,
164}
165
166pub struct ViewAccount {
167    pub(crate) account_id: AccountId,
168}
169
170pub struct ViewBlock;
171
172pub struct ViewState {
173    account_id: AccountId,
174    prefix: Option<Vec<u8>>,
175}
176
177pub struct ViewAccessKey {
178    pub(crate) account_id: AccountId,
179    pub(crate) public_key: PublicKey,
180}
181
182pub struct ViewAccessKeyList {
183    pub(crate) account_id: AccountId,
184}
185
186pub struct GasPrice;
187
188impl ProcessQuery for ViewFunction {
189    type Method = methods::query::RpcQueryRequest;
190    type Output = ViewResultDetails;
191
192    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
193        Ok(Self::Method {
194            block_reference,
195            request: QueryRequest::CallFunction {
196                account_id: self.account_id,
197                method_name: self.function.name,
198                args: self.function.args?.into(),
199            },
200        })
201    }
202
203    fn from_response(resp: RpcQueryResponse) -> Result<Self::Output> {
204        match resp.kind {
205            QueryResponseKind::CallResult(result) => Ok(result.into()),
206            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying account")),
207        }
208    }
209}
210
211// Specific builder methods attached to a ViewFunction.
212impl Query<'_, ViewFunction> {
213    /// Provide the arguments for the call. These args are serialized bytes from either
214    /// a JSON or Borsh serializable set of arguments. To use the more specific versions
215    /// with better quality of life, use `args_json` or `args_borsh`.
216    pub fn args(mut self, args: Vec<u8>) -> Self {
217        self.method.function = self.method.function.args(args);
218        self
219    }
220
221    /// Similar to `args`, specify an argument that is JSON serializable and can be
222    /// accepted by the equivalent contract. Recommend to use something like
223    /// `serde_json::json!` macro to easily serialize the arguments.
224    pub fn args_json<U: serde::Serialize>(mut self, args: U) -> Self {
225        self.method.function = self.method.function.args_json(args);
226        self
227    }
228
229    /// Similar to `args`, specify an argument that is borsh serializable and can be
230    /// accepted by the equivalent contract.
231    pub fn args_borsh<U: unc_primitives::borsh::BorshSerialize>(mut self, args: U) -> Self {
232        self.method.function = self.method.function.args_borsh(args);
233        self
234    }
235}
236
237impl ProcessQuery for ViewCode {
238    type Method = methods::query::RpcQueryRequest;
239    type Output = Vec<u8>;
240
241    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
242        Ok(Self::Method {
243            block_reference,
244            request: QueryRequest::ViewCode {
245                account_id: self.account_id,
246            },
247        })
248    }
249
250    fn from_response(resp: RpcQueryResponse) -> Result<Self::Output> {
251        match resp.kind {
252            QueryResponseKind::ViewCode(contract) => Ok(contract.code),
253            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying code")),
254        }
255    }
256}
257
258impl ProcessQuery for ViewAccount {
259    type Method = methods::query::RpcQueryRequest;
260    type Output = AccountDetails;
261
262    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
263        Ok(Self::Method {
264            block_reference,
265            request: QueryRequest::ViewAccount {
266                account_id: self.account_id,
267            },
268        })
269    }
270
271    fn from_response(resp: RpcQueryResponse) -> Result<Self::Output> {
272        match resp.kind {
273            QueryResponseKind::ViewAccount(account) => Ok(account.into()),
274            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying account")),
275        }
276    }
277}
278
279impl ProcessQuery for ViewBlock {
280    type Method = methods::block::RpcBlockRequest;
281    type Output = Block;
282
283    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
284        Ok(Self::Method { block_reference })
285    }
286
287    fn from_response(view: BlockView) -> Result<Self::Output> {
288        Ok(view.into())
289    }
290}
291
292impl ProcessQuery for ViewState {
293    type Method = methods::query::RpcQueryRequest;
294    type Output = HashMap<Vec<u8>, Vec<u8>>;
295
296    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
297        Ok(Self::Method {
298            block_reference,
299            request: QueryRequest::ViewState {
300                account_id: self.account_id,
301                prefix: StoreKey::from(self.prefix.map(Vec::from).unwrap_or_default()),
302                include_proof: false,
303            },
304        })
305    }
306
307    fn from_response(resp: <Self::Method as RpcMethod>::Response) -> Result<Self::Output> {
308        match resp.kind {
309            QueryResponseKind::ViewState(state) => Ok(tool::into_state_map(state.values)),
310            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying state")),
311        }
312    }
313}
314
315impl<'a> Query<'a, ViewState> {
316    pub(crate) fn view_state(client: &'a Client, id: &AccountId) -> Self {
317        Self::new(
318            client,
319            ViewState {
320                account_id: id.clone(),
321                prefix: None,
322            },
323        )
324    }
325
326    /// Set the prefix for viewing the state.
327    pub fn prefix(mut self, value: &[u8]) -> Self {
328        self.method.prefix = Some(value.into());
329        self
330    }
331}
332
333impl ProcessQuery for ViewAccessKey {
334    type Method = methods::query::RpcQueryRequest;
335    type Output = AccessKey;
336
337    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
338        Ok(Self::Method {
339            block_reference,
340            request: QueryRequest::ViewAccessKey {
341                account_id: self.account_id,
342                public_key: self.public_key.into(),
343            },
344        })
345    }
346
347    fn from_response(resp: <Self::Method as RpcMethod>::Response) -> Result<Self::Output> {
348        match resp.kind {
349            QueryResponseKind::AccessKey(key) => Ok(key.into()),
350            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying access key")),
351        }
352    }
353}
354
355impl ProcessQuery for ViewAccessKeyList {
356    type Method = methods::query::RpcQueryRequest;
357    type Output = Vec<AccessKeyInfo>;
358
359    fn into_request(self, block_reference: BlockReference) -> Result<Self::Method> {
360        Ok(Self::Method {
361            block_reference,
362            request: QueryRequest::ViewAccessKeyList {
363                account_id: self.account_id,
364            },
365        })
366    }
367
368    fn from_response(resp: <Self::Method as RpcMethod>::Response) -> Result<Self::Output> {
369        match resp.kind {
370            QueryResponseKind::AccessKeyList(keylist) => {
371                Ok(keylist.keys.into_iter().map(Into::into).collect())
372            }
373            _ => Err(RpcErrorCode::QueryReturnedInvalidData.message("while querying access keys")),
374        }
375    }
376}
377
378impl ProcessQuery for GasPrice {
379    type Method = methods::gas_price::RpcGasPriceRequest;
380    type Output = UncToken;
381
382    fn into_request(self, block_ref: BlockReference) -> Result<Self::Method> {
383        let block_id = match block_ref {
384            // User provided input via `block_hash` or `block_height` functions.
385            BlockReference::BlockId(block_id) => Some(block_id),
386            // default case, set by `Query` struct via BlockReference::latest.
387            BlockReference::Finality(_finality) => None,
388            // Should not be reachable, unless code got changed.
389            BlockReference::SyncCheckpoint(point) => {
390                return Err(RpcErrorCode::QueryFailure.message(format!(
391                    "Cannot supply sync checkpoint to gas price: {point:?}. Potential API bug?"
392                )))
393            }
394        };
395
396        Ok(Self::Method { block_id })
397    }
398
399    fn from_response(resp: <Self::Method as RpcMethod>::Response) -> Result<Self::Output> {
400        Ok(UncToken::from_attounc(resp.gas_price))
401    }
402}
403
404/// Query object used to query for chunk related details at a specific `ChunkReference` which
405/// consists of either a chunk [`CryptoHash`], or a `BlockShardId` (which consists of [`ShardId`]
406/// and either block [`CryptoHash`] or [`BlockHeight`]).
407///
408/// The default behavior where a `ChunkReference` is not supplied will use a `BlockShardId`
409/// referencing the latest block `CryptoHash` with `ShardId` of 0.
410pub struct QueryChunk<'a> {
411    client: &'a Client,
412    chunk_ref: Option<ChunkReference>,
413}
414
415impl<'a> QueryChunk<'a> {
416    pub(crate) fn new(client: &'a Client) -> Self {
417        Self {
418            client,
419            chunk_ref: None,
420        }
421    }
422
423    /// Specify at which block hash and shard id to query the chunk from. Note that only
424    /// archival networks will have the full history while networks like mainnet or testnet
425    /// will only have the history from 5 or less epochs ago.
426    pub fn block_hash_and_shard(mut self, hash: CryptoHash, shard_id: ShardId) -> Self {
427        self.chunk_ref = Some(ChunkReference::BlockShardId {
428            block_id: BlockId::Hash(unc_primitives::hash::CryptoHash(hash.0)),
429            shard_id,
430        });
431        self
432    }
433
434    /// Specify at which block height and shard id to query the chunk from. Note that only
435    /// archival networks will have the full history while networks like mainnet or testnet
436    /// will only have the history from 5 or less epochs ago.
437    pub fn block_height_and_shard(mut self, height: BlockHeight, shard_id: ShardId) -> Self {
438        self.chunk_ref = Some(ChunkReference::BlockShardId {
439            block_id: BlockId::Height(height),
440            shard_id,
441        });
442        self
443    }
444
445    /// Specify at which chunk hash to query the chunk from.
446    pub fn chunk_hash(mut self, hash: CryptoHash) -> Self {
447        self.chunk_ref = Some(ChunkReference::ChunkHash {
448            chunk_id: unc_primitives::hash::CryptoHash(hash.0),
449        });
450        self
451    }
452}
453
454impl<'a> std::future::IntoFuture for QueryChunk<'a> {
455    type Output = Result<Chunk>;
456    type IntoFuture = BoxFuture<'a, Self::Output>;
457
458    fn into_future(self) -> Self::IntoFuture {
459        Box::pin(async move {
460            let chunk_reference = if let Some(chunk_ref) = self.chunk_ref {
461                chunk_ref
462            } else {
463                // Use the latest block hash in the case the user doesn't supply the ChunkReference. Note that
464                // shard_id 0 is used in the default case.
465                let block_view = self.client.view_block(None).await?;
466                ChunkReference::BlockShardId {
467                    block_id: BlockId::Hash(block_view.header.hash),
468                    shard_id: 0,
469                }
470            };
471
472            let chunk_view = self
473                .client
474                .query(methods::chunk::RpcChunkRequest { chunk_reference })
475                .await
476                .map_err(|e| RpcErrorCode::QueryFailure.custom(e))?;
477
478            Ok(chunk_view.into())
479        })
480    }
481}