concordium_rust_sdk/cis2/
mod.rs

1//! This module contains types and functions for interacting with smart
2//! contracts following the [CIS-2](https://proposals.concordium.software/CIS/cis-2.html) specification.
3//!
4//! The type [`Cis2Contract`] act as a wrapper around
5//! the [Client](crate::v2::Client) and a contract address providing
6//! functions for querying and making transactions to smart contract.
7mod types;
8
9use crate::{contract_client::*, types as sdk_types, v2::IntoBlockIdentifier};
10use concordium_base::{
11    base::Energy,
12    contracts_common::{Address, Amount},
13    transactions::{AccountTransaction, EncodedPayload},
14};
15use sdk_types::{smart_contracts, transactions};
16use smart_contracts::concordium_contracts_common;
17use std::convert::From;
18use thiserror::*;
19pub use types::*;
20
21#[derive(Debug, Clone, Copy)]
22/// A marker type to indicate that a [`ContractClient`] is a client for a `CIS2`
23/// contract.
24pub enum Cis2Type {}
25
26/// A wrapper around the client representing a CIS2 token smart contract, which
27/// provides functions for interaction specific to CIS2 contracts.
28///
29/// Note that cloning is cheap and is, therefore, the intended way of sharing
30/// this type between multiple tasks.
31///
32/// See also [`ContractClient`] for generic methods available for any contract.
33pub type Cis2Contract = ContractClient<Cis2Type>;
34
35/// Error which can occur when submitting a transaction such as `transfer` and
36/// `updateOperator` to a CIS2 smart contract.
37#[derive(Debug, Error)]
38pub enum Cis2TransactionError {
39    /// The smart contract receive name is invalid.
40    #[error("Invalid receive name: {0}")]
41    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
42
43    /// The parameter for `transfer` is invalid.
44    #[error("Invalid transfer parameter: {0}")]
45    InvalidTransferParams(#[from] NewTransferParamsError),
46
47    /// The parameter for `updateOperator` is invalid.
48    #[error("Invalid updateOperator parameter: {0}")]
49    InvalidUpdateOperatorParams(#[from] NewUpdateOperatorParamsError),
50
51    /// A general RPC error occured.
52    #[error("RPC error: {0}")]
53    RPCError(#[from] crate::endpoints::RPCError),
54}
55
56/// Error which can occur when submitting a transaction such as `transfer` and
57/// `updateOperator` to a CIS2 smart contract.
58#[derive(Debug, Error)]
59pub enum Cis2DryRunError {
60    /// The smart contract receive name is invalid.
61    #[error("Invalid receive name: {0}")]
62    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
63
64    /// The parameter for `transfer` is invalid.
65    #[error("Invalid transfer parameter: {0}")]
66    InvalidTransferParams(#[from] NewTransferParamsError),
67
68    /// The parameter for `updateOperator` is invalid.
69    #[error("Invalid updateOperator parameter: {0}")]
70    InvalidUpdateOperatorParams(#[from] NewUpdateOperatorParamsError),
71
72    /// An error occurred when querying the node.
73    #[error("RPC error: {0}")]
74    QueryError(#[from] crate::endpoints::QueryError),
75
76    /// The node rejected the invocation.
77    #[error("Rejected by the node: {0:?}.")]
78    NodeRejected(sdk_types::RejectReason),
79}
80
81/// Error which can occur when invoking a query such as `balanceOf` and
82/// `operatorOf` or `tokenMetadata` to a CIS2 smart contract.
83#[derive(Debug, Error)]
84pub enum Cis2QueryError {
85    /// The smart contract receive name is invalid.
86    #[error("Invalid receive name: {0}")]
87    InvalidReceiveName(#[from] concordium_contracts_common::NewReceiveNameError),
88
89    /// The parameter for `balanceOf` is invalid.
90    #[error("Invalid balanceOf parameter: {0}")]
91    InvalidBalanceOfParams(#[from] NewBalanceOfQueryParamsError),
92
93    /// The parameter for `operatorOf` is invalid.
94    #[error("Invalid operatorOf parameter: {0}")]
95    InvalidOperatorOfParams(#[from] NewOperatorOfQueryParamsError),
96
97    /// The parameter for `tokenMetadata` is invalid.
98    #[error("Invalid tokenMetadata parameter: {0}")]
99    InvalidTokenMetadataParams(#[from] NewTokenMetadataQueryParamsError),
100
101    /// A general RPC error occured.
102    #[error("RPC error: {0}")]
103    RPCError(#[from] super::v2::QueryError),
104
105    /// The returned bytes from invoking the smart contract could not be parsed.
106    #[error("Failed parsing the response.")]
107    ResponseParseError(#[from] concordium_contracts_common::ParseError),
108
109    /// The node rejected the invocation.
110    #[error("Rejected by the node: {0:?}.")]
111    NodeRejected(sdk_types::RejectReason),
112}
113
114// This is implemented manually, since deriving it using thiserror requires
115// `RejectReason` to implement std::error::Error.
116impl From<sdk_types::RejectReason> for Cis2QueryError {
117    fn from(err: sdk_types::RejectReason) -> Self { Self::NodeRejected(err) }
118}
119
120// This is implemented manually, since deriving it using thiserror requires
121// `RejectReason` to implement std::error::Error.
122impl From<sdk_types::RejectReason> for Cis2DryRunError {
123    fn from(err: sdk_types::RejectReason) -> Self { Self::NodeRejected(err) }
124}
125
126/// Transaction metadata for CIS-2
127pub type Cis2TransactionMetadata = ContractTransactionMetadata;
128
129impl Cis2Contract {
130    /// Like [`transfer`](Self::transfer) except it only dry-runs the
131    /// transaction to get the response and, in case of success, amount of
132    /// energy used for execution.
133    ///
134    /// # Arguments
135    ///
136    /// * `bi` - The block to dry-run at. The invocation happens at the end of
137    ///   the specified block.
138    /// * `sender` - The address that is invoking the entrypoint.
139    /// * `transfers` - A list of CIS2 token transfers to execute.
140    pub async fn transfer_dry_run(
141        &mut self,
142        bi: impl IntoBlockIdentifier,
143        sender: Address,
144        transfers: Vec<Transfer>,
145    ) -> Result<Energy, Cis2DryRunError> {
146        let parameter = TransferParams::new(transfers)?;
147        let parameter = smart_contracts::OwnedParameter::from_serial(&parameter)
148            .map_err(|_| Cis2DryRunError::InvalidTransferParams(NewTransferParamsError))?;
149        let ir = self
150            .invoke_raw::<Cis2DryRunError>("transfer", Amount::zero(), Some(sender), parameter, bi)
151            .await?;
152        match ir {
153            smart_contracts::InvokeContractResult::Success { used_energy, .. } => Ok(used_energy),
154            smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
155        }
156    }
157
158    /// Like [`transfer_dry_run`](Self::transfer_dry_run) except it is more
159    /// ergonomic when only a single transfer is to be made.
160    pub async fn transfer_single_dry_run(
161        &mut self,
162        bi: impl IntoBlockIdentifier,
163        sender: Address,
164        transfer: Transfer,
165    ) -> Result<Energy, Cis2DryRunError> {
166        self.transfer_dry_run(bi, sender, vec![transfer]).await
167    }
168
169    /// Construct **and send** a CIS2 transfer smart contract update transaction
170    /// given a list of CIS2 transfers. Returns a Result with the
171    /// transaction hash.
172    ///
173    /// # Arguments
174    ///
175    /// * `signer` - The account keys to use for signing the smart contract
176    ///   update transaction.
177    /// * `transaction_metadata` - Metadata for constructing the transaction.
178    /// * `transfers` - A list of CIS2 token transfers to execute.
179    pub async fn transfer(
180        &mut self,
181        signer: &impl transactions::ExactSizeTransactionSigner,
182        transaction_metadata: Cis2TransactionMetadata,
183        transfers: Vec<Transfer>,
184    ) -> Result<sdk_types::hashes::TransactionHash, Cis2TransactionError> {
185        let transfer = self.make_transfer(signer, transaction_metadata, transfers)?;
186        let hash = self.client.send_account_transaction(transfer).await?;
187        Ok(hash)
188    }
189
190    /// Construct a CIS2 transfer smart contract update transaction
191    /// given a list of CIS2 transfers. Returns a [`Result`] with an account
192    /// transaction that can be sent.
193    ///
194    /// # Arguments
195    ///
196    /// * `signer` - The account keys to use for signing the smart contract
197    ///   update transaction.
198    /// * `transaction_metadata` - Metadata for constructing the transaction.
199    /// * `transfers` - A list of CIS2 token transfers to execute.
200    pub fn make_transfer(
201        &self,
202        signer: &impl transactions::ExactSizeTransactionSigner,
203        transaction_metadata: Cis2TransactionMetadata,
204        transfers: Vec<Transfer>,
205    ) -> Result<AccountTransaction<EncodedPayload>, Cis2TransactionError> {
206        let parameter = TransferParams::new(transfers)?;
207        let message = smart_contracts::OwnedParameter::from_serial(&parameter)
208            .map_err(|_| Cis2TransactionError::InvalidTransferParams(NewTransferParamsError))?;
209        self.make_update_raw(signer, &transaction_metadata, "transfer", message)
210    }
211
212    /// Like [`transfer`](Self::transfer), except it is more ergonomic
213    /// when transferring a single token.
214    pub async fn transfer_single(
215        &mut self,
216        signer: &impl transactions::ExactSizeTransactionSigner,
217        transaction_metadata: Cis2TransactionMetadata,
218        transfer: Transfer,
219    ) -> Result<sdk_types::hashes::TransactionHash, Cis2TransactionError> {
220        self.transfer(signer, transaction_metadata, vec![transfer])
221            .await
222    }
223
224    /// Like [`make_transfer`](Self::make_transfer), except it is more ergonomic
225    /// when transferring a single token.
226    pub fn make_transfer_single(
227        &self,
228        signer: &impl transactions::ExactSizeTransactionSigner,
229        transaction_metadata: Cis2TransactionMetadata,
230        transfer: Transfer,
231    ) -> Result<AccountTransaction<EncodedPayload>, Cis2TransactionError> {
232        self.make_transfer(signer, transaction_metadata, vec![transfer])
233    }
234
235    /// Dry run a CIS2 updateOperator transaction. This is analogous to
236    /// [`update_operator`](Self::update_operator), except that it does not send
237    /// a transaction to the chain, and just simulates the transaction.
238    ///
239    /// # Arguments
240    ///
241    /// * `bi` - The block to dry-run at. The invocation happens at the end of
242    /// * `owner` - The address that is invoking. This is the owner of the
243    ///   tokens.
244    /// * `updates` - A list of CIS2 UpdateOperators to update.
245    pub async fn update_operator_dry_run(
246        &mut self,
247        bi: impl IntoBlockIdentifier,
248        owner: Address,
249        updates: Vec<UpdateOperator>,
250    ) -> anyhow::Result<Energy, Cis2DryRunError> {
251        let parameter = UpdateOperatorParams::new(updates)?;
252        let parameter = smart_contracts::OwnedParameter::from_serial(&parameter)
253            .map_err(|_| Cis2DryRunError::InvalidTransferParams(NewTransferParamsError))?;
254        let ir = self
255            .invoke_raw::<Cis2DryRunError>(
256                "updateOperator",
257                Amount::zero(),
258                Some(owner),
259                parameter,
260                bi,
261            )
262            .await?;
263        match ir {
264            smart_contracts::InvokeContractResult::Success { used_energy, .. } => Ok(used_energy),
265            smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
266        }
267    }
268
269    /// Like [`update_operator_dry_run`](Self::update_operator_dry_run) except
270    /// more ergonomic when a single operator is to be updated.
271    pub async fn update_operator_single_dry_run(
272        &mut self,
273        bi: impl IntoBlockIdentifier,
274        owner: Address,
275        operator: Address,
276        update: OperatorUpdate,
277    ) -> anyhow::Result<Energy, Cis2DryRunError> {
278        self.update_operator_dry_run(bi, owner, vec![UpdateOperator { update, operator }])
279            .await
280    }
281
282    /// Construct a CIS2 updateOperator smart contract update
283    /// transaction given a list of CIS2 UpdateOperators. Returns a [`Result`]
284    /// with the account transaction that can be sent.
285    ///
286    /// # Arguments
287    ///
288    /// * `signer` - The account keys to use for signing the smart contract
289    ///   update transaction.
290    /// * `transaction_metadata` - Metadata for constructing the transaction.
291    /// * `updates` - A list of CIS2 UpdateOperators to update.
292    pub fn make_update_operator(
293        &self,
294        signer: &impl transactions::ExactSizeTransactionSigner,
295        transaction_metadata: Cis2TransactionMetadata,
296        updates: Vec<UpdateOperator>,
297    ) -> anyhow::Result<AccountTransaction<EncodedPayload>, Cis2TransactionError> {
298        let parameter = UpdateOperatorParams::new(updates)?;
299        let message = smart_contracts::OwnedParameter::from_serial(&parameter).map_err(|_| {
300            Cis2TransactionError::InvalidUpdateOperatorParams(NewUpdateOperatorParamsError)
301        })?;
302        self.make_update_raw(signer, &transaction_metadata, "updateOperator", message)
303    }
304
305    /// Construct **and send** a CIS2 updateOperator smart contract update
306    /// transaction given a list of CIS2 UpdateOperators. Returns a Result
307    /// with the transaction hash.
308    ///
309    /// # Arguments
310    ///
311    /// * `signer` - The account keys to use for signing the smart contract
312    ///   update transaction.
313    /// * `transaction_metadata` - Metadata for constructing the transaction.
314    /// * `updates` - A list of CIS2 UpdateOperators to update.
315    pub async fn update_operator(
316        &mut self,
317        signer: &impl transactions::ExactSizeTransactionSigner,
318        transaction_metadata: Cis2TransactionMetadata,
319        updates: Vec<UpdateOperator>,
320    ) -> anyhow::Result<sdk_types::hashes::TransactionHash, Cis2TransactionError> {
321        let update = self.make_update_operator(signer, transaction_metadata, updates)?;
322        let hash = self.client.send_account_transaction(update).await?;
323        Ok(hash)
324    }
325
326    /// Like [`update_operator`](Self::update_operator), but more ergonomic
327    /// when updating a single operator.
328    pub async fn update_operator_single(
329        &mut self,
330        signer: &impl transactions::ExactSizeTransactionSigner,
331        transaction_metadata: Cis2TransactionMetadata,
332        operator: Address,
333        update: OperatorUpdate,
334    ) -> anyhow::Result<sdk_types::hashes::TransactionHash, Cis2TransactionError> {
335        self.update_operator(signer, transaction_metadata, vec![UpdateOperator {
336            update,
337            operator,
338        }])
339        .await
340    }
341
342    /// Like [`make_update_operator`](Self::make_update_operator), but more
343    /// ergonomic when updating a single operator.
344    pub fn make_update_operator_single(
345        &self,
346        signer: &impl transactions::ExactSizeTransactionSigner,
347        transaction_metadata: Cis2TransactionMetadata,
348        operator: Address,
349        update: OperatorUpdate,
350    ) -> anyhow::Result<AccountTransaction<EncodedPayload>, Cis2TransactionError> {
351        self.make_update_operator(signer, transaction_metadata, vec![UpdateOperator {
352            update,
353            operator,
354        }])
355    }
356
357    /// Invoke the CIS2 balanceOf query given a list of BalanceOfQuery.
358    ///
359    /// Note: the query is executed locally by the node and does not produce a
360    /// transaction on-chain.
361    ///
362    /// # Arguments
363    ///
364    /// * `bi` - The block to query. The query will be executed in the state of
365    ///   the chain at the end of the block.
366    /// * `queries` - A list queries to execute.
367    pub async fn balance_of(
368        &mut self,
369        bi: impl IntoBlockIdentifier,
370        queries: Vec<BalanceOfQuery>,
371    ) -> Result<BalanceOfQueryResponse, Cis2QueryError> {
372        let parameter = BalanceOfQueryParams::new(queries)?;
373        let parameter = smart_contracts::OwnedParameter::from_serial(&parameter)
374            .map_err(|_| Cis2QueryError::InvalidBalanceOfParams(NewBalanceOfQueryParamsError))?;
375        self.view_raw("balanceOf", parameter, bi).await
376    }
377
378    /// Like [`balance_of`](Self::balance_of), except for querying a single
379    /// token. This additionally ensures that the response has correct
380    /// length.
381    pub async fn balance_of_single(
382        &mut self,
383        bi: impl IntoBlockIdentifier,
384        token_id: TokenId,
385        address: Address,
386    ) -> Result<TokenAmount, Cis2QueryError> {
387        let res = self
388            .balance_of(bi, vec![BalanceOfQuery { token_id, address }])
389            .await?;
390        only_one(res)
391    }
392
393    /// Invoke the CIS2 operatorOf query given a list of OperatorOfQuery.
394    ///
395    /// Note: the query is executed locally by the node and does not produce a
396    /// transaction on-chain.
397    ///
398    /// # Arguments
399    ///
400    /// * `bi` - The block to query. The query will be executed in the state of
401    ///   the chain at the end of the block.
402    /// * `queries` - A list queries to execute.
403    pub async fn operator_of(
404        &mut self,
405        bi: impl IntoBlockIdentifier,
406        queries: Vec<OperatorOfQuery>,
407    ) -> Result<OperatorOfQueryResponse, Cis2QueryError> {
408        let parameter = OperatorOfQueryParams::new(queries)?;
409        let parameter = smart_contracts::OwnedParameter::from_serial(&parameter)
410            .map_err(|_| Cis2QueryError::InvalidOperatorOfParams(NewOperatorOfQueryParamsError))?;
411        self.view_raw("operatorOf", parameter, bi).await
412    }
413
414    /// Like [`operator_of`](Self::operator_of), except for querying a single
415    /// `owner`-`address` pair. This additionally ensures that the response
416    /// has correct length.
417    pub async fn operator_of_single(
418        &mut self,
419        bi: impl IntoBlockIdentifier,
420        owner: Address,
421        operator: Address,
422    ) -> Result<bool, Cis2QueryError> {
423        let res = self
424            .operator_of(bi, vec![OperatorOfQuery {
425                owner,
426                address: operator,
427            }])
428            .await?;
429        only_one(res)
430    }
431
432    /// Invoke the CIS2 tokenMetadata query given a list of CIS2 TokenIds.
433    ///
434    /// Note: the query is executed locally by the node and does not produce a
435    /// transaction on-chain.
436    ///
437    /// # Arguments
438    ///
439    /// * `bi` - The block to query. The query will be executed in the state of
440    ///   the chain at the end of the block.
441    /// * `queries` - A list queries to execute.
442    pub async fn token_metadata(
443        &mut self,
444        bi: impl IntoBlockIdentifier,
445        queries: Vec<TokenId>,
446    ) -> Result<TokenMetadataQueryResponse, Cis2QueryError> {
447        let parameter = TokenMetadataQueryParams::new(queries)?;
448        let parameter = smart_contracts::OwnedParameter::from_serial(&parameter).map_err(|_| {
449            Cis2QueryError::InvalidTokenMetadataParams(NewTokenMetadataQueryParamsError)
450        })?;
451        self.view_raw("tokenMetadata", parameter, bi).await
452    }
453
454    /// Like [`token_metadata`](Self::token_metadata), except for querying a
455    /// single token. This additionally ensures that the response has
456    /// correct length.
457    pub async fn token_metadata_single(
458        &mut self,
459        bi: impl IntoBlockIdentifier,
460        token_id: TokenId,
461    ) -> Result<MetadataUrl, Cis2QueryError> {
462        let res = self.token_metadata(bi, vec![token_id]).await?;
463        only_one(res)
464    }
465}
466
467/// Extract an element from the given vector if the vector has exactly one
468/// element. Otherwise raise a parse error.
469fn only_one<A, V: AsRef<Vec<A>>>(res: V) -> Result<A, Cis2QueryError>
470where
471    Vec<A>: From<V>, {
472    let err = Cis2QueryError::ResponseParseError(concordium_contracts_common::ParseError {});
473    if res.as_ref().len() > 1 {
474        Err(err)
475    } else {
476        Vec::from(res).pop().ok_or(err)
477    }
478}