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