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