identity_iota_core/rebased/client/
full_client.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ops::Deref;
5
6use crate::iota_interaction_adapter::IotaClientAdapter;
7use crate::rebased::client::QueryControlledDidsError;
8use crate::rebased::iota::move_calls;
9use crate::rebased::iota::package::identity_package_id;
10use crate::rebased::migration::get_identity_impl;
11use crate::rebased::migration::ControllerToken;
12use crate::rebased::migration::CreateIdentity;
13use crate::rebased::migration::IdentityResolutionError;
14use crate::rebased::migration::InsufficientControllerVotingPower;
15use crate::rebased::migration::NotAController;
16use crate::rebased::migration::OnChainIdentity;
17use crate::IotaDID;
18use crate::IotaDocument;
19use crate::StateMetadataDocument;
20use crate::StateMetadataEncoding;
21use async_trait::async_trait;
22use identity_verification::jwk::Jwk;
23use iota_interaction::move_types::language_storage::StructTag;
24use iota_interaction::rpc_types::IotaObjectData;
25use iota_interaction::rpc_types::IotaObjectDataFilter;
26use iota_interaction::rpc_types::IotaObjectResponseQuery;
27use iota_interaction::rpc_types::IotaTransactionBlockEffects;
28use iota_interaction::types::base_types::IotaAddress;
29use iota_interaction::types::base_types::ObjectRef;
30use iota_interaction::types::crypto::PublicKey;
31use iota_interaction::types::transaction::ProgrammableTransaction;
32#[cfg(not(target_arch = "wasm32"))]
33use iota_interaction::IotaClient;
34#[cfg(target_arch = "wasm32")]
35use iota_interaction_ts::bindings::WasmIotaClient as IotaClient;
36use product_common::core_client::CoreClient;
37use product_common::core_client::CoreClientReadOnly;
38use product_common::network_name::NetworkName;
39use product_common::transaction::transaction_builder::Transaction;
40use product_common::transaction::transaction_builder::TransactionBuilder;
41use secret_storage::Signer;
42use serde::de::DeserializeOwned;
43use tokio::sync::OnceCell;
44use tokio::sync::RwLock;
45
46use super::get_object_id_from_did;
47use crate::rebased::assets::AuthenticatedAssetBuilder;
48use crate::rebased::migration::Identity;
49use crate::rebased::migration::IdentityBuilder;
50use crate::rebased::Error;
51use iota_interaction::types::base_types::ObjectID;
52use iota_interaction::IotaClientTrait;
53use iota_interaction::IotaKeySignature;
54use iota_interaction::MoveType;
55use iota_interaction::OptionalSync;
56
57use super::IdentityClientReadOnly;
58
59/// Mirrored types from identity_storage::KeyId
60#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
61pub struct KeyId(String);
62
63impl KeyId {
64  /// Creates a new key identifier from a string.
65  pub fn new(id: impl Into<String>) -> Self {
66    Self(id.into())
67  }
68
69  /// Returns string representation of the key id.
70  pub fn as_str(&self) -> &str {
71    &self.0
72  }
73}
74
75impl std::fmt::Display for KeyId {
76  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77    f.write_str(&self.0)
78  }
79}
80
81impl From<KeyId> for String {
82  fn from(value: KeyId) -> Self {
83    value.0
84  }
85}
86
87/// A marker type indicating the absence of a signer.
88#[derive(Debug, Clone, Copy)]
89#[non_exhaustive]
90pub struct NoSigner;
91
92/// A client for interacting with the IOTA Identity framework.
93#[derive(Clone)]
94pub struct IdentityClient<S = NoSigner> {
95  /// [`IdentityClientReadOnly`] instance, used for read-only operations.
96  pub(super) read_client: IdentityClientReadOnly,
97  /// The public key of the client.
98  /// # Safety
99  /// `public_key` is always `Some` when `S: Signer<IotaKeySignature>`.
100  /// Developers MUST uphold this invariant.
101  pub(super) public_key: Option<PublicKey>,
102  /// The signer of the client.
103  pub(super) signer: S,
104}
105
106impl<S> Deref for IdentityClient<S> {
107  type Target = IdentityClientReadOnly;
108  fn deref(&self) -> &Self::Target {
109    &self.read_client
110  }
111}
112
113/// The error that results from a failed attempt at creating an [IdentityClient]
114/// from a given [IotaClient].
115#[derive(Debug, thiserror::Error)]
116#[error("failed to create an 'IdentityClient' from the given 'IotaClient'")]
117#[non_exhaustive]
118pub struct FromIotaClientError {
119  /// Type of failure for this error.
120  #[source]
121  pub kind: FromIotaClientErrorKind,
122}
123
124/// Types of failure for [FromIotaClientError].
125#[derive(Debug, thiserror::Error)]
126#[non_exhaustive]
127pub enum FromIotaClientErrorKind {
128  /// A package ID is required, but was not supplied.
129  #[error("an IOTA Identity package ID must be supplied when connecting to an unofficial IOTA network")]
130  MissingPackageId,
131  /// Network ID resolution through an RPC call failed.
132  #[error("failed to resolve the network the given client is connected to")]
133  NetworkResolution(#[source] Box<dyn std::error::Error + Send + Sync>),
134}
135
136impl IdentityClient<NoSigner> {
137  /// Creates a new [IdentityClient], with **no** signing capabilities, from the given [IotaClient].
138  ///
139  /// # Warning
140  /// Passing a `custom_package_id` is **only** required when connecting to a custom IOTA network.
141  ///
142  /// Relying on a custom Identity package when connected to an official IOTA network is **highly
143  /// discouraged** and is sure to result in compatibility issues when interacting with other official
144  /// IOTA Trust Framework's products.
145  ///
146  /// # Examples
147  /// ```
148  /// # use identity_iota_core::rebased::client::IdentityClient;
149  ///
150  /// # #[tokio::main]
151  /// # async fn main() -> anyhow::Result<()> {
152  /// let iota_client = iota_sdk::IotaClientBuilder::default()
153  ///   .build_testnet()
154  ///   .await?;
155  /// // No package ID is required since we are connecting to an official IOTA network.
156  /// let identity_client = IdentityClient::from_iota_client(iota_client, None).await?;
157  /// # Ok(())
158  /// # }
159  /// ```
160  pub async fn from_iota_client(
161    iota_client: IotaClient,
162    custom_package_id: impl Into<Option<ObjectID>>,
163  ) -> Result<Self, FromIotaClientError> {
164    let read_only_client = if let Some(custom_package_id) = custom_package_id.into() {
165      IdentityClientReadOnly::new_with_pkg_id(iota_client, custom_package_id).await
166    } else {
167      IdentityClientReadOnly::new(iota_client).await
168    }
169    .map_err(|e| match e {
170      Error::InvalidConfig(_) => FromIotaClientErrorKind::MissingPackageId,
171      Error::RpcError(msg) => FromIotaClientErrorKind::NetworkResolution(msg.into()),
172      _ => unreachable!("'IdentityClientReadOnly::new' has been changed without updating error handling in 'IdentityClient::from_iota_client'"),
173    })
174    .map_err(|kind| FromIotaClientError { kind })?;
175
176    Ok(Self {
177      read_client: read_only_client,
178      public_key: None,
179      signer: NoSigner,
180    })
181  }
182}
183
184impl<S> IdentityClient<S>
185where
186  S: Signer<IotaKeySignature>,
187{
188  /// Creates a new [`IdentityClient`].
189  #[deprecated(since = "1.9.0", note = "Use `IdentityClient::from_iota_client` instead")]
190  pub async fn new(client: IdentityClientReadOnly, signer: S) -> Result<Self, Error> {
191    let public_key = signer
192      .public_key()
193      .await
194      .map_err(|e| Error::InvalidKey(e.to_string()))?;
195
196    Ok(Self {
197      public_key: Some(public_key),
198      read_client: client,
199      signer,
200    })
201  }
202
203  /// Returns a reference to the [PublicKey] wrapped by this client.
204  pub fn public_key(&self) -> &PublicKey {
205    self.public_key.as_ref().expect("public_key is set")
206  }
207
208  /// Returns the [IotaAddress] wrapped by this client.
209  #[inline(always)]
210  pub fn address(&self) -> IotaAddress {
211    IotaAddress::from(self.public_key())
212  }
213
214  /// Returns the list of **all** unique DIDs the address wrapped by this client can access as a controller.
215  pub async fn controlled_dids(&self) -> Result<Vec<IotaDID>, QueryControlledDidsError> {
216    self.dids_controlled_by(self.address()).await
217  }
218}
219
220impl<S> IdentityClient<S> {
221  /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`].
222  pub fn create_identity(&self, iota_document: IotaDocument) -> IdentityBuilder {
223    IdentityBuilder::new(iota_document)
224  }
225
226  /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`].
227  pub fn create_authenticated_asset<T>(&self, content: T) -> AuthenticatedAssetBuilder<T>
228  where
229    T: MoveType + DeserializeOwned + Send + Sync + PartialEq,
230  {
231    AuthenticatedAssetBuilder::new(content)
232  }
233
234  /// Sets a new signer for this client.
235  pub async fn with_signer<NewS>(self, signer: NewS) -> Result<IdentityClient<NewS>, secret_storage::Error>
236  where
237    NewS: Signer<IotaKeySignature>,
238  {
239    let public_key = signer.public_key().await?;
240
241    Ok(IdentityClient {
242      read_client: self.read_client,
243      public_key: Some(public_key),
244      signer,
245    })
246  }
247}
248
249impl<S> IdentityClient<S>
250where
251  S: Signer<IotaKeySignature> + OptionalSync,
252{
253  /// Returns a [PublishDidDocument] transaction wrapped by a [TransactionBuilder].
254  pub fn publish_did_document(&self, document: IotaDocument) -> TransactionBuilder<PublishDidDocument> {
255    TransactionBuilder::new(PublishDidDocument::new(document, self.sender_address()))
256  }
257
258  // TODO: define what happens for (legacy|migrated|new) documents
259  /// Updates a DID Document.
260  pub async fn publish_did_document_update(
261    &self,
262    document: IotaDocument,
263    gas_budget: u64,
264  ) -> Result<IotaDocument, Error> {
265    let mut oci =
266      if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(document.id())?).await? {
267        value
268      } else {
269        return Err(Error::Identity("only new identities can be updated".to_string()));
270      };
271
272    let controller_token = oci.get_controller_token(self).await?.ok_or_else(|| {
273      Error::Identity(format!(
274        "address {} has no control over Identity {}",
275        self.sender_address(),
276        oci.id()
277      ))
278    })?;
279
280    oci
281      .update_did_document(document.clone(), &controller_token)
282      .finish(self)
283      .await?
284      .with_gas_budget(gas_budget)
285      .build_and_execute(self)
286      .await
287      .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?;
288
289    Ok(document)
290  }
291
292  /// Deactivates a DID document.
293  pub async fn deactivate_did_output(&self, did: &IotaDID, gas_budget: u64) -> Result<(), Error> {
294    let mut oci = if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(did)?).await? {
295      value
296    } else {
297      return Err(Error::Identity("only new identities can be deactivated".to_string()));
298    };
299
300    let controller_token = oci.get_controller_token(self).await?.ok_or_else(|| {
301      Error::Identity(format!(
302        "address {} has no control over Identity {}",
303        self.sender_address(),
304        oci.id()
305      ))
306    })?;
307
308    oci
309      .deactivate_did(&controller_token)
310      .finish(self)
311      .await?
312      .with_gas_budget(gas_budget)
313      .build_and_execute(self)
314      .await
315      .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?;
316
317    Ok(())
318  }
319
320  /// A shorthand for
321  /// [OnChainIdentity::update_did_document](crate::rebased::migration::OnChainIdentity::update_did_document)'s DID
322  /// Document.
323  ///
324  /// This method makes the following assumptions:
325  /// - The given `did_document` has already been published on-chain within an Identity.
326  /// - This [IdentityClient] is a controller of the corresponding Identity with enough voting power to execute the
327  ///   transaction without any other controller approval.
328  pub async fn publish_did_update(
329    &self,
330    did_document: IotaDocument,
331  ) -> Result<TransactionBuilder<ShorthandDidUpdate>, MakeUpdateDidDocTxError> {
332    use MakeUpdateDidDocTxError as Error;
333    use MakeUpdateDidDocTxErrorKind as ErrorKind;
334
335    let make_err = |kind| Error {
336      did_document: did_document.clone(),
337      kind,
338    };
339
340    let identity_id = did_document.id().to_object_id();
341    let identity = get_identity_impl(self, identity_id)
342      .await
343      .map_err(|e| make_err(e.into()))?;
344
345    if identity.has_deleted_did() {
346      return Err(make_err(ErrorKind::DeletedIdentityDocument));
347    }
348
349    let controller_token = identity
350      .get_controller_token(self)
351      .await
352      .map_err(|e| make_err(ErrorKind::RpcError(e.into())))?
353      .ok_or_else(|| {
354        make_err(
355          NotAController {
356            address: self.address(),
357            identity: did_document.id().clone(),
358          }
359          .into(),
360        )
361      })?;
362
363    let vp = identity
364      .controller_voting_power(controller_token.controller_id())
365      .expect("is a controller");
366    let threshold = identity.threshold();
367    if vp < threshold {
368      return Err(make_err(
369        InsufficientControllerVotingPower {
370          controller_token_id: controller_token.controller_id(),
371          controller_voting_power: vp,
372          required: threshold,
373        }
374        .into(),
375      ));
376    }
377
378    Ok(TransactionBuilder::new(ShorthandDidUpdate {
379      identity: RwLock::new(identity),
380      controller_token,
381      did_document,
382    }))
383  }
384
385  /// Query the objects owned by the address wrapped by this client to find the object of type `tag`
386  /// and that satisfies `predicate`.
387  pub async fn find_owned_ref<P>(&self, tag: StructTag, predicate: P) -> Result<Option<ObjectRef>, Error>
388  where
389    P: Fn(&IotaObjectData) -> bool,
390  {
391    let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag));
392
393    let mut cursor = None;
394    loop {
395      let mut page = self
396        .read_api()
397        .get_owned_objects(self.sender_address(), Some(filter.clone()), cursor, None)
398        .await?;
399      let obj_ref = std::mem::take(&mut page.data)
400        .into_iter()
401        .filter_map(|res| res.data)
402        .find(|obj| predicate(obj))
403        .map(|obj_data| obj_data.object_ref());
404      cursor = page.next_cursor;
405
406      if obj_ref.is_some() {
407        return Ok(obj_ref);
408      }
409      if !page.has_next_page {
410        break;
411      }
412    }
413
414    Ok(None)
415  }
416}
417
418#[cfg_attr(feature = "send-sync", async_trait)]
419#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
420impl<S> CoreClientReadOnly for IdentityClient<S>
421where
422  S: OptionalSync,
423{
424  fn client_adapter(&self) -> &IotaClientAdapter {
425    &self.read_client
426  }
427
428  fn package_id(&self) -> ObjectID {
429    self.read_client.package_id()
430  }
431
432  fn package_history(&self) -> Vec<ObjectID> {
433    self.read_client.package_history()
434  }
435
436  fn network_name(&self) -> &NetworkName {
437    self.read_client.network()
438  }
439}
440
441impl<S> CoreClient<S> for IdentityClient<S>
442where
443  S: Signer<IotaKeySignature> + OptionalSync,
444{
445  fn sender_address(&self) -> IotaAddress {
446    IotaAddress::from(self.public_key())
447  }
448
449  fn signer(&self) -> &S {
450    &self.signer
451  }
452
453  fn sender_public_key(&self) -> &PublicKey {
454    self.public_key()
455  }
456}
457
458/// Utility function that returns the key's bytes of a JWK encoded public ed25519 key.
459pub fn get_sender_public_key(sender_public_jwk: &Jwk) -> Result<Vec<u8>, Error> {
460  let public_key_base_64 = &sender_public_jwk
461    .try_okp_params()
462    .map_err(|err| Error::InvalidKey(format!("key not of type `Okp`; {err}")))?
463    .x;
464
465  identity_jose::jwu::decode_b64(public_key_base_64)
466    .map_err(|err| Error::InvalidKey(format!("could not decode base64 public key; {err}")))
467}
468
469/// Publishes a new DID Document on-chain. An [`crate::rebased::migration::OnChainIdentity`] will be created to contain
470/// the provided document.
471#[derive(Debug, Clone)]
472pub struct PublishDidDocument {
473  did_document: IotaDocument,
474  controller: IotaAddress,
475  cached_ptb: OnceCell<ProgrammableTransaction>,
476}
477
478impl PublishDidDocument {
479  /// Creates a new [PublishDidDocument] transaction.
480  pub fn new(did_document: IotaDocument, controller: IotaAddress) -> Self {
481    Self {
482      did_document,
483      controller,
484      cached_ptb: OnceCell::new(),
485    }
486  }
487
488  async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
489    let package = identity_package_id(client).await?;
490    let did_doc = StateMetadataDocument::from(self.did_document.clone())
491      .pack(StateMetadataEncoding::Json)
492      .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
493
494    let programmable_tx_bcs =
495      move_calls::identity::new_with_controllers(Some(&did_doc), [(self.controller, 1, false)], 1, package).await?;
496    Ok(bcs::from_bytes(&programmable_tx_bcs)?)
497  }
498}
499
500#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
501#[cfg_attr(feature = "send-sync", async_trait)]
502impl Transaction for PublishDidDocument {
503  type Output = IotaDocument;
504  type Error = Error;
505
506  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
507  where
508    C: CoreClientReadOnly + OptionalSync,
509  {
510    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
511  }
512
513  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
514  where
515    C: CoreClientReadOnly + OptionalSync,
516  {
517    let tx = {
518      let builder = IdentityBuilder::new(self.did_document)
519        .threshold(1)
520        .controller(self.controller, 1);
521      CreateIdentity::new(builder)
522    };
523
524    tx.apply(effects, client).await.map(IotaDocument::from)
525  }
526}
527
528/// The actual Transaction type returned by [IdentityClient::publish_did_update].
529#[derive(Debug)]
530pub struct ShorthandDidUpdate {
531  identity: RwLock<OnChainIdentity>,
532  controller_token: ControllerToken,
533  did_document: IotaDocument,
534}
535
536#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
537#[cfg_attr(feature = "send-sync", async_trait)]
538impl Transaction for ShorthandDidUpdate {
539  type Error = Error;
540  type Output = IotaDocument;
541
542  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
543  where
544    C: CoreClientReadOnly + OptionalSync,
545  {
546    let mut identity = self.identity.write().await;
547    let ptb = identity
548      .update_did_document(self.did_document.clone(), &self.controller_token)
549      .finish(client)
550      .await?
551      .into_inner()
552      .ptb;
553
554    Ok(ptb)
555  }
556
557  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
558  where
559    C: CoreClientReadOnly + OptionalSync,
560  {
561    let mut identity = self.identity.into_inner();
562    let _ = identity
563      .update_did_document(self.did_document, &self.controller_token)
564      .finish(client)
565      .await?
566      .into_inner()
567      .apply(effects, client)
568      .await?;
569    Ok(identity.did_doc)
570  }
571}
572
573/// [IdentityClient::publish_did_update] error.
574#[derive(Debug, thiserror::Error)]
575#[error("failed to prepare transaction to update DID '{}'", did_document.id())]
576#[non_exhaustive]
577pub struct MakeUpdateDidDocTxError {
578  /// The DID document that was being published.
579  pub did_document: IotaDocument,
580  /// Specific type of failure for this error.
581  pub kind: MakeUpdateDidDocTxErrorKind,
582}
583
584/// Types of failure for [MakeUpdateDidDocTxError].
585#[derive(Debug, thiserror::Error)]
586#[non_exhaustive]
587pub enum MakeUpdateDidDocTxErrorKind {
588  /// Node RPC failure.
589  #[error(transparent)]
590  RpcError(Box<dyn std::error::Error + Send + Sync>),
591  /// Failed to resolve the corresponding [OnChainIdentity].
592  #[error(transparent)]
593  IdentityResolution(#[from] IdentityResolutionError),
594  /// The invoking client is not a controller of the given DID document.
595  #[error(transparent)]
596  NotAController(#[from] NotAController),
597  /// The DID document has been deleted and cannot be updated.
598  #[error("Identity's DID Document is deleted")]
599  DeletedIdentityDocument,
600  /// The invoking client is a controller but doesn't have enough voting power
601  /// to perform the update.
602  #[error(transparent)]
603  InsufficientVotingPower(#[from] InsufficientControllerVotingPower),
604}