concordium_rust_sdk/
cis3.rs

1//! This module contains types and functions for interacting with smart
2//! contracts following the [CIS-3](https://proposals.concordium.software/CIS/cis-3.html) specification.
3use crate::{
4    contract_client::{ContractClient, ContractTransactionMetadata},
5    types::{self as sdk_types, transactions},
6    v2::{self, IntoBlockIdentifier},
7};
8use concordium_base::{
9    base::Energy,
10    cis3_types::{
11        NewSupportsPermitQueryParamsError, PermitParams, SupportsPermitQueryParams,
12        SupportsPermitRepsonse,
13    },
14    contracts_common::{Address, Amount, OwnedEntrypointName},
15    smart_contracts::OwnedParameter,
16    transactions::{AccountTransaction, EncodedPayload},
17};
18use sdk_types::smart_contracts;
19use smart_contracts::concordium_contracts_common;
20use thiserror::Error;
21
22#[derive(Debug, Clone, Copy)]
23/// A marker type to indicate that a [`ContractClient`] is a client for a `CIS3`
24/// contract.
25pub enum Cis3Type {}
26
27/// A wrapper around the client representing a CIS3 token smart contract, which
28/// provides functions for interaction specific to CIS3 contracts.
29///
30/// Note that cloning is cheap and is, therefore, the intended way of sharing
31/// this type between multiple tasks.
32///
33/// See also [`ContractClient`] for generic methods available for any contract.
34pub type Cis3Contract = ContractClient<Cis3Type>;
35
36/// Error which can occur when calling [`permit`](Cis3Contract::permit).
37#[derive(Error, Debug)]
38pub enum Cis3PermitError {
39    /// The smart contract receive name is invalid.
40    #[error("Invalid receive name: {0}")]
41    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
42
43    /// A general RPC error occurred.
44    #[error("RPC error: {0}")]
45    RPCError(#[from] crate::endpoints::RPCError),
46}
47
48/// Error which can occur when calling [`permit`](Cis3Contract::permit_dry_run).
49#[derive(Error, Debug)]
50pub enum Cis3PermitDryRunError {
51    /// The smart contract receive name is invalid.
52    #[error("Invalid receive name: {0}")]
53    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
54
55    /// A general RPC error occurred.
56    #[error("RPC error: {0}")]
57    RPCError(#[from] crate::endpoints::RPCError),
58
59    /// An error occurred when querying the node.
60    #[error("RPC error: {0}")]
61    QueryError(#[from] crate::endpoints::QueryError),
62
63    /// The node rejected the invocation.
64    #[error("Rejected by the node: {0:?}.")]
65    NodeRejected(v2::Upward<sdk_types::RejectReason>),
66}
67
68/// Error which can occur when calling
69/// [`supportsPermit`](Cis3Contract::supports_permit).
70#[derive(Debug, Error)]
71pub enum Cis3SupportsPermitError {
72    /// The smart contract receive name is invalid.
73    #[error("Invalid receive name: {0}")]
74    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
75
76    /// The parameter is invalid.
77    #[error("Invalid supportsPermit parameter: {0}")]
78    InvalidParams(#[from] NewSupportsPermitQueryParamsError),
79
80    /// A general RPC error occurred.
81    #[error("RPC error: {0}")]
82    RPCError(#[from] super::v2::QueryError),
83
84    /// The returned bytes from invoking the smart contract could not be parsed.
85    #[error("Failed parsing the response.")]
86    ResponseParseError(#[from] concordium_contracts_common::ParseError),
87
88    /// The node rejected the invocation.
89    #[error("Rejected by the node: {0:?}.")]
90    NodeRejected(v2::Upward<sdk_types::RejectReason>),
91}
92
93// This is implemented manually, since deriving it using thiserror requires
94// `RejectReason` to implement std::error::Error.
95impl From<v2::Upward<sdk_types::RejectReason>> for Cis3SupportsPermitError {
96    fn from(err: v2::Upward<sdk_types::RejectReason>) -> Self {
97        Self::NodeRejected(err)
98    }
99}
100impl From<sdk_types::RejectReason> for Cis3SupportsPermitError {
101    fn from(err: sdk_types::RejectReason) -> Self {
102        Self::NodeRejected(v2::Upward::Known(err))
103    }
104}
105
106// This is implemented manually, since deriving it using thiserror requires
107// `RejectReason` to implement std::error::Error.
108impl From<v2::Upward<sdk_types::RejectReason>> for Cis3PermitDryRunError {
109    fn from(err: v2::Upward<sdk_types::RejectReason>) -> Self {
110        Self::NodeRejected(err)
111    }
112}
113impl From<sdk_types::RejectReason> for Cis3PermitDryRunError {
114    fn from(err: sdk_types::RejectReason) -> Self {
115        Self::NodeRejected(v2::Upward::Known(err))
116    }
117}
118
119impl Cis3Contract {
120    /// Like [`permit`](Self::permit) except it only dry-runs the transaction
121    /// to get the response and, in case of success, amount of energy used
122    /// for execution.
123    ///
124    /// # Arguments
125    ///
126    /// * `bi` - The block to query. The query will be executed in the state of
127    ///   the chain at the end of the block.
128    /// * `sender` - The (sponsor) address that is invoking the entrypoint.
129    /// * `params` - The parameters for the permit invocation. Includes the
130    ///   signature of the sponsoree, the address of the sponsoree, and the
131    ///   signed message.
132    pub async fn permit_dry_run(
133        &mut self,
134        bi: impl IntoBlockIdentifier,
135        sender: Address,
136        params: PermitParams,
137    ) -> Result<Energy, Cis3PermitDryRunError> {
138        let parameter = OwnedParameter::from_serial(&params)
139            .expect("A PermitParams should always be serializable");
140        let ir = self
141            .invoke_raw::<Cis3PermitDryRunError>(
142                "permit",
143                Amount::zero(),
144                Some(sender),
145                parameter,
146                bi,
147            )
148            .await?;
149        match ir {
150            smart_contracts::InvokeContractResult::Success { used_energy, .. } => Ok(used_energy),
151            smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
152        }
153    }
154
155    /// Construct **and send** a CIS3 sponsored transaction. This function takes
156    /// a signature from the sponsoree along with their account address, and
157    /// the signed message to be executed by the contract. Returns a [`Result`]
158    /// with the transaction hash.
159    ///
160    /// # Arguments
161    ///
162    /// * `signer` - The account keys (of the sponsor) to use for signing the
163    ///   smart contract update transaction.
164    /// * `metadata` - Metadata for constructing the transaction.
165    /// * `params` - The parameters for the permit invocation. Includes the
166    ///   signature of the sponsoree, the address of the sponsoree, and the
167    ///   signed message.
168    pub async fn permit(
169        &mut self,
170        signer: &impl transactions::ExactSizeTransactionSigner,
171        metadata: &ContractTransactionMetadata,
172        params: PermitParams,
173    ) -> Result<sdk_types::hashes::TransactionHash, Cis3PermitError> {
174        let permit = self.make_permit(signer, metadata, params)?;
175        let hash = self.client.send_account_transaction(permit).await?;
176        Ok(hash)
177    }
178
179    /// Construct a CIS3 sponsored transaction. This function takes a signature
180    /// from the sponsoree along with their account address, and the signed
181    /// message to be executed by the contract. Returns a [`Result`] with the
182    /// transaction hash.
183    ///
184    /// # Arguments
185    ///
186    /// * `signer` - The account keys (of the sponsor) to use for signing the
187    ///   smart contract update transaction.
188    /// * `metadata` - Metadata for constructing the transaction.
189    /// * `params` - The parameters for the permit invocation. Includes the
190    ///   signature of the sponsoree, the address of the sponsoree, and the
191    ///   signed message.
192    pub fn make_permit(
193        &mut self,
194        signer: &impl transactions::ExactSizeTransactionSigner,
195        metadata: &ContractTransactionMetadata,
196        params: PermitParams,
197    ) -> Result<AccountTransaction<EncodedPayload>, Cis3PermitError> {
198        let message = smart_contracts::OwnedParameter::from_serial(&params)
199            .expect("A PermitParams should always be serializable");
200        self.make_update_raw(signer, metadata, "permit", message)
201    }
202
203    /// Invoke the CIS3 `supportsPermit` query given a list of entrypoints to
204    /// query.
205    ///
206    /// Note: the query is executed locally by the node and does not produce a
207    /// transaction on-chain.
208    ///
209    /// # Arguments
210    ///
211    /// * `bi` - The block to query. The query will be executed in the state of
212    ///   the chain at the end of the block.
213    /// * `entrypoints` - A list queries to execute.
214    pub async fn supports_permit(
215        &mut self,
216        bi: impl IntoBlockIdentifier,
217        entrypoints: Vec<OwnedEntrypointName>,
218    ) -> Result<SupportsPermitRepsonse, Cis3SupportsPermitError> {
219        let parameter = SupportsPermitQueryParams::new(entrypoints)?;
220        let message = OwnedParameter::from_serial(&parameter).map_err(|_| {
221            Cis3SupportsPermitError::InvalidParams(NewSupportsPermitQueryParamsError)
222        })?;
223        self.view_raw("supportsPermit", message, bi).await
224    }
225
226    /// Like [`supports_permit`](Self::supports_permit), but only queries a
227    /// single entrypoint, and returns a bool indicating whether the entrypoint
228    /// is supported.
229    pub async fn supports_permit_single(
230        &mut self,
231        bi: impl IntoBlockIdentifier,
232        entrypoint: OwnedEntrypointName,
233    ) -> Result<bool, Cis3SupportsPermitError> {
234        only_one(self.supports_permit(bi, vec![entrypoint]).await?)
235    }
236}
237
238/// Extract an element from the given vector if the vector has exactly one
239/// element. Otherwise raise a parse error.
240fn only_one<A, V: AsRef<Vec<A>>>(res: V) -> Result<A, Cis3SupportsPermitError>
241where
242    Vec<A>: From<V>,
243{
244    let err =
245        Cis3SupportsPermitError::ResponseParseError(concordium_contracts_common::ParseError {});
246    if res.as_ref().len() > 1 {
247        Err(err)
248    } else {
249        Vec::from(res).pop().ok_or(err)
250    }
251}