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