concordium_rust_sdk/
cis4.rs

1//! This module contains types and functions for interacting with smart
2//! contracts following the [CIS-4](https://proposals.concordium.software/CIS/cis-4.html) specification.
3//!
4//! The type [`Cis4Contract`] acts as a wrapper
5//! around the [Client](crate::v2::Client) and a contract address providing
6//! functions for querying and making transactions to smart contract.
7
8use crate::{
9    contract_client::*,
10    types::{transactions, RejectReason},
11    v2::{self, IntoBlockIdentifier},
12};
13pub use concordium_base::{cis2_types::MetadataUrl, cis4_types::*};
14use concordium_base::{
15    constants::MAX_PARAMETER_LEN,
16    contracts_common,
17    hashes::TransactionHash,
18    smart_contracts::{ExceedsParameterSize, OwnedParameter},
19    transactions::{AccountTransaction, EncodedPayload},
20    web3id::{CredentialHolderId, Web3IdSigner, REVOKE_DOMAIN_STRING},
21};
22
23#[derive(thiserror::Error, Debug)]
24/// An error that can occur when executing CIS4 queries.
25pub enum Cis4QueryError {
26    /// The smart contract receive name is invalid.
27    #[error("Invalid receive name: {0}")]
28    InvalidReceiveName(#[from] contracts_common::NewReceiveNameError),
29
30    /// A general RPC error occurred.
31    #[error("RPC error: {0}")]
32    RPCError(#[from] super::v2::QueryError),
33
34    /// The data returned from q query could not be parsed.
35    #[error("Failed parsing the response.")]
36    ResponseParseError(#[from] contracts_common::ParseError),
37
38    /// The node rejected the invocation.
39    #[error("Rejected by the node: {0:?}.")]
40    NodeRejected(v2::Upward<crate::types::RejectReason>),
41}
42
43impl From<v2::Upward<RejectReason>> for Cis4QueryError {
44    fn from(value: v2::Upward<RejectReason>) -> Self {
45        Self::NodeRejected(value)
46    }
47}
48impl From<RejectReason> for Cis4QueryError {
49    fn from(value: RejectReason) -> Self {
50        Self::NodeRejected(v2::Upward::Known(value))
51    }
52}
53
54impl Cis4QueryError {
55    /// Check if the error variant is a logic error, i.e., the query
56    /// was received by the node which attempted to execute it, and it failed.
57    /// If so, extract the reason for execution failure.
58    pub fn is_contract_error(&self) -> Option<v2::Upward<&crate::types::RejectReason>> {
59        if let Self::NodeRejected(e) = self {
60            Some(e.as_ref())
61        } else {
62            None
63        }
64    }
65}
66
67#[derive(thiserror::Error, Debug)]
68/// An error that can occur when sending CIS4 update transactions.
69pub enum Cis4TransactionError {
70    /// The smart contract receive name is invalid.
71    #[error("Invalid receive name: {0}")]
72    InvalidReceiveName(#[from] contracts_common::NewReceiveNameError),
73
74    /// The parameter is too large.
75    #[error("Parameter is too large: {0}")]
76    InvalidParams(#[from] ExceedsParameterSize),
77
78    /// A general RPC error occurred.
79    #[error("RPC error: {0}")]
80    RPCError(#[from] super::v2::RPCError),
81
82    /// The node rejected the invocation.
83    #[error("Rejected by the node: {0:?}.")]
84    NodeRejected(v2::Upward<crate::types::RejectReason>),
85}
86
87/// Transaction metadata for CIS-4 update transactions.
88pub type Cis4TransactionMetadata = ContractTransactionMetadata;
89
90#[derive(Debug, Clone, Copy)]
91/// A marker type to indicate that a [`ContractClient`] is a client for a `CIS4`
92/// contract.
93pub enum Cis4Type {}
94
95/// A wrapper around the client representing a CIS4 credential registry smart
96/// contract.
97///
98/// Note that cloning is cheap and is, therefore, the intended way of sharing
99/// this type between multiple tasks.
100///
101/// See also [`ContractClient`] for generic methods available for any contract.
102pub type Cis4Contract = ContractClient<Cis4Type>;
103
104impl Cis4Contract {
105    /// Look up an entry in the registry by its id.
106    pub async fn credential_entry(
107        &mut self,
108        cred_id: CredentialHolderId,
109        bi: impl IntoBlockIdentifier,
110    ) -> Result<CredentialEntry, Cis4QueryError> {
111        let parameter =
112            OwnedParameter::from_serial(&cred_id).expect("Credential ID is a valid parameter.");
113
114        self.view_raw("credentialEntry", parameter, bi).await
115    }
116
117    /// Look up the status of a credential by its id.
118    pub async fn credential_status(
119        &mut self,
120        cred_id: CredentialHolderId,
121        bi: impl IntoBlockIdentifier,
122    ) -> Result<CredentialStatus, Cis4QueryError> {
123        let parameter =
124            OwnedParameter::from_serial(&cred_id).expect("Credential ID is a valid parameter.");
125
126        self.view_raw("credentialStatus", parameter, bi).await
127    }
128
129    /// Get the list of all the revocation keys together with their nonces.
130    pub async fn revocation_keys(
131        &mut self,
132        bi: impl IntoBlockIdentifier,
133    ) -> Result<Vec<RevocationKeyWithNonce>, Cis4QueryError> {
134        let parameter = OwnedParameter::empty();
135
136        self.view_raw("revocationKeys", parameter, bi).await
137    }
138
139    /// Look up the credential registry's metadata.
140    pub async fn registry_metadata(
141        &mut self,
142        bi: impl IntoBlockIdentifier,
143    ) -> Result<RegistryMetadata, Cis4QueryError> {
144        let parameter = OwnedParameter::empty();
145        self.view_raw("registryMetadata", parameter, bi).await
146    }
147
148    /// Look up the issuer's public key.
149    pub async fn issuer(
150        &mut self,
151        bi: impl IntoBlockIdentifier,
152    ) -> Result<IssuerKey, Cis4QueryError> {
153        let parameter = OwnedParameter::empty();
154
155        self.view_raw("issuer", parameter, bi).await
156    }
157
158    /// Construct a transaction for registering a new credential.
159    /// Note that this **does not** send the transaction.c
160    pub fn make_register_credential(
161        &self,
162        signer: &impl transactions::ExactSizeTransactionSigner,
163        metadata: &Cis4TransactionMetadata,
164        cred_info: &CredentialInfo,
165        additional_data: &[u8],
166    ) -> Result<AccountTransaction<EncodedPayload>, Cis4TransactionError> {
167        use contracts_common::Serial;
168        let mut payload = contracts_common::to_bytes(cred_info);
169        let actual = payload.len() + additional_data.len() + 2;
170        if payload.len() + additional_data.len() + 2 > MAX_PARAMETER_LEN {
171            return Err(Cis4TransactionError::InvalidParams(ExceedsParameterSize {
172                actual,
173                max: MAX_PARAMETER_LEN,
174            }));
175        }
176        (additional_data.len() as u16)
177            .serial(&mut payload)
178            .expect("We checked lengths above, so this must succeed.");
179        payload.extend_from_slice(additional_data);
180        let parameter = OwnedParameter::try_from(payload)?;
181        self.make_update_raw(signer, metadata, "registerCredential", parameter)
182    }
183
184    /// Register a new credential.
185    pub async fn register_credential(
186        &mut self,
187        signer: &impl transactions::ExactSizeTransactionSigner,
188        metadata: &Cis4TransactionMetadata,
189        cred_info: &CredentialInfo,
190        additional_data: &[u8],
191    ) -> Result<TransactionHash, Cis4TransactionError> {
192        let tx = self.make_register_credential(signer, metadata, cred_info, additional_data)?;
193        let hash = self.client.send_account_transaction(tx).await?;
194        Ok(hash)
195    }
196
197    /// Construct a transaction to revoke a credential as an issuer.
198    pub fn make_revoke_credential_as_issuer(
199        &self,
200        signer: &impl transactions::ExactSizeTransactionSigner,
201        metadata: &Cis4TransactionMetadata,
202        cred_id: CredentialHolderId,
203        reason: Option<Reason>,
204    ) -> Result<AccountTransaction<EncodedPayload>, Cis4TransactionError> {
205        let parameter = OwnedParameter::from_serial(&(cred_id, reason))?;
206
207        self.make_update_raw(signer, metadata, "revokeCredentialIssuer", parameter)
208    }
209
210    /// Revoke a credential as an issuer.
211    pub async fn revoke_credential_as_issuer(
212        &mut self,
213        signer: &impl transactions::ExactSizeTransactionSigner,
214        metadata: &Cis4TransactionMetadata,
215        cred_id: CredentialHolderId,
216        reason: Option<Reason>,
217    ) -> Result<TransactionHash, Cis4TransactionError> {
218        let tx = self.make_revoke_credential_as_issuer(signer, metadata, cred_id, reason)?;
219        let hash = self.client.send_account_transaction(tx).await?;
220        Ok(hash)
221    }
222
223    /// Revoke a credential as the holder.
224    ///
225    /// The extra nonce that must be provided is the holder's nonce inside the
226    /// contract. The signature on this revocation message is set to expire at
227    /// the same time as the transaction.
228    pub async fn revoke_credential_as_holder(
229        &mut self,
230        signer: &impl transactions::ExactSizeTransactionSigner,
231        metadata: &Cis4TransactionMetadata,
232        web3signer: impl Web3IdSigner, // the holder
233        nonce: u64,
234        reason: Option<Reason>,
235    ) -> Result<TransactionHash, Cis4TransactionError> {
236        let tx =
237            self.make_revoke_credential_as_holder(signer, metadata, web3signer, nonce, reason)?;
238        let hash = self.client.send_account_transaction(tx).await?;
239        Ok(hash)
240    }
241
242    /// Revoke a credential as the holder.
243    ///
244    /// The extra nonce that must be provided is the holder's nonce inside the
245    /// contract. The signature on this revocation message is set to expire at
246    /// the same time as the transaction.
247    pub fn make_revoke_credential_as_holder(
248        &self,
249        signer: &impl transactions::ExactSizeTransactionSigner,
250        metadata: &Cis4TransactionMetadata,
251        web3signer: impl Web3IdSigner, // the holder
252        nonce: u64,
253        reason: Option<Reason>,
254    ) -> Result<AccountTransaction<EncodedPayload>, Cis4TransactionError> {
255        use contracts_common::Serial;
256        let mut to_sign = REVOKE_DOMAIN_STRING.to_vec();
257        let cred_id: CredentialHolderId = web3signer.id().into();
258        cred_id
259            .serial(&mut to_sign)
260            .expect("Serialization to vector does not fail.");
261        self.address
262            .serial(&mut to_sign)
263            .expect("Serialization to vector does not fail.");
264        nonce
265            .serial(&mut to_sign)
266            .expect("Serialization to vector does not fail.");
267        metadata
268            .expiry
269            .seconds
270            .checked_mul(1000)
271            .unwrap_or(u64::MAX)
272            .serial(&mut to_sign)
273            .expect("Serialization to vector does not fail.");
274        reason
275            .serial(&mut to_sign)
276            .expect("Serialization to vector does not fail.");
277        let sig = web3signer.sign(&to_sign);
278        let mut parameter_vec = sig.to_bytes().to_vec();
279        parameter_vec.extend_from_slice(&to_sign[REVOKE_DOMAIN_STRING.len()..]);
280        let parameter = OwnedParameter::try_from(parameter_vec)?;
281
282        self.make_update_raw(signer, metadata, "revokeCredentialHolder", parameter)
283    }
284
285    /// Revoke a credential as another party, distinct from issuer or holder.
286    ///
287    /// The extra nonce that must be provided is the nonce associated with the
288    /// key that signs the revocation message.
289    /// The signature on this revocation message is set to expire at
290    /// the same time as the transaction.
291    pub async fn revoke_credential_other(
292        &mut self,
293        signer: &impl transactions::ExactSizeTransactionSigner,
294        metadata: &Cis4TransactionMetadata,
295        revoker: impl Web3IdSigner, // the revoker.
296        nonce: u64,
297        cred_id: CredentialHolderId,
298        reason: Option<&Reason>,
299    ) -> Result<TransactionHash, Cis4TransactionError> {
300        let tx =
301            self.make_revoke_credential_other(signer, metadata, revoker, nonce, cred_id, reason)?;
302        let hash = self.client.send_account_transaction(tx).await?;
303        Ok(hash)
304    }
305
306    /// Construct a transaction to revoke a credential as another party,
307    /// distinct from issuer or holder.
308    ///
309    /// The extra nonce that must be provided is the nonce associated with the
310    /// key that signs the revocation message.
311    /// The signature on this revocation message is set to expire at
312    /// the same time as the transaction.
313    pub fn make_revoke_credential_other(
314        &self,
315        signer: &impl transactions::ExactSizeTransactionSigner,
316        metadata: &Cis4TransactionMetadata,
317        revoker: impl Web3IdSigner, // the revoker.
318        nonce: u64,
319        cred_id: CredentialHolderId,
320        reason: Option<&Reason>,
321    ) -> Result<AccountTransaction<EncodedPayload>, Cis4TransactionError> {
322        use contracts_common::Serial;
323        let mut to_sign = REVOKE_DOMAIN_STRING.to_vec();
324        cred_id
325            .serial(&mut to_sign)
326            .expect("Serialization to vector does not fail.");
327        self.address
328            .serial(&mut to_sign)
329            .expect("Serialization to vector does not fail.");
330        nonce
331            .serial(&mut to_sign)
332            .expect("Serialization to vector does not fail.");
333        metadata
334            .expiry
335            .seconds
336            .checked_mul(1000)
337            .unwrap_or(u64::MAX)
338            .serial(&mut to_sign)
339            .expect("Serialization to vector does not fail.");
340        RevocationKey::from(revoker.id())
341            .serial(&mut to_sign)
342            .expect("Serialization to vector does not fail.");
343        reason
344            .serial(&mut to_sign)
345            .expect("Serialization to vector does not fail.");
346        let sig = revoker.sign(&to_sign);
347        let mut parameter_vec = sig.to_bytes().to_vec();
348        parameter_vec.extend_from_slice(&to_sign[REVOKE_DOMAIN_STRING.len()..]);
349        let parameter = OwnedParameter::try_from(parameter_vec)?;
350
351        self.make_update_raw(signer, metadata, "revokeCredentialOther", parameter)
352    }
353}