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::CreateIdentity;
11use crate::IotaDID;
12use crate::IotaDocument;
13use crate::StateMetadataDocument;
14use crate::StateMetadataEncoding;
15use async_trait::async_trait;
16use identity_verification::jwk::Jwk;
17use iota_interaction::move_types::language_storage::StructTag;
18use iota_interaction::rpc_types::IotaObjectData;
19use iota_interaction::rpc_types::IotaObjectDataFilter;
20use iota_interaction::rpc_types::IotaObjectResponseQuery;
21use iota_interaction::rpc_types::IotaTransactionBlockEffects;
22use iota_interaction::types::base_types::IotaAddress;
23use iota_interaction::types::base_types::ObjectRef;
24use iota_interaction::types::crypto::PublicKey;
25use iota_interaction::types::transaction::ProgrammableTransaction;
26use product_common::core_client::CoreClient;
27use product_common::core_client::CoreClientReadOnly;
28use product_common::network_name::NetworkName;
29use product_common::transaction::transaction_builder::Transaction;
30use product_common::transaction::transaction_builder::TransactionBuilder;
31use secret_storage::Signer;
32use serde::de::DeserializeOwned;
33use tokio::sync::OnceCell;
34
35use super::get_object_id_from_did;
36use crate::rebased::assets::AuthenticatedAssetBuilder;
37use crate::rebased::migration::Identity;
38use crate::rebased::migration::IdentityBuilder;
39use crate::rebased::Error;
40use iota_interaction::types::base_types::ObjectID;
41use iota_interaction::IotaClientTrait;
42use iota_interaction::IotaKeySignature;
43use iota_interaction::MoveType;
44use iota_interaction::OptionalSync;
45
46use super::IdentityClientReadOnly;
47
48/// Mirrored types from identity_storage::KeyId
49#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
50pub struct KeyId(String);
51
52impl KeyId {
53  /// Creates a new key identifier from a string.
54  pub fn new(id: impl Into<String>) -> Self {
55    Self(id.into())
56  }
57
58  /// Returns string representation of the key id.
59  pub fn as_str(&self) -> &str {
60    &self.0
61  }
62}
63
64impl std::fmt::Display for KeyId {
65  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66    f.write_str(&self.0)
67  }
68}
69
70impl From<KeyId> for String {
71  fn from(value: KeyId) -> Self {
72    value.0
73  }
74}
75
76/// A client for interacting with the IOTA network.
77#[derive(Clone)]
78pub struct IdentityClient<S> {
79  /// [`IdentityClientReadOnly`] instance, used for read-only operations.
80  read_client: IdentityClientReadOnly,
81  /// The public key of the client.
82  public_key: PublicKey,
83  /// The signer of the client.
84  signer: S,
85}
86
87impl<S> Deref for IdentityClient<S> {
88  type Target = IdentityClientReadOnly;
89  fn deref(&self) -> &Self::Target {
90    &self.read_client
91  }
92}
93
94impl<S> IdentityClient<S>
95where
96  S: Signer<IotaKeySignature>,
97{
98  /// Create a new [`IdentityClient`].
99  pub async fn new(client: IdentityClientReadOnly, signer: S) -> Result<Self, Error> {
100    let public_key = signer
101      .public_key()
102      .await
103      .map_err(|e| Error::InvalidKey(e.to_string()))?;
104
105    Ok(Self {
106      public_key,
107      read_client: client,
108      signer,
109    })
110  }
111}
112
113impl<S> IdentityClient<S> {
114  /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`].
115  pub fn create_identity(&self, iota_document: IotaDocument) -> IdentityBuilder {
116    IdentityBuilder::new(iota_document)
117  }
118
119  /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`].
120  pub fn create_authenticated_asset<T>(&self, content: T) -> AuthenticatedAssetBuilder<T>
121  where
122    T: MoveType + DeserializeOwned + Send + Sync + PartialEq,
123  {
124    AuthenticatedAssetBuilder::new(content)
125  }
126
127  /// Returns the [IotaAddress] wrapped by this client.
128  #[inline(always)]
129  pub fn address(&self) -> IotaAddress {
130    IotaAddress::from(&self.public_key)
131  }
132
133  /// Returns the list of **all** unique DIDs the address wrapped by this client can access as a controller.
134  pub async fn controlled_dids(&self) -> Result<Vec<IotaDID>, QueryControlledDidsError> {
135    self.dids_controlled_by(self.address()).await
136  }
137}
138
139impl<S> IdentityClient<S>
140where
141  S: Signer<IotaKeySignature> + OptionalSync,
142{
143  /// Returns a [PublishDidDocument] transaction wrapped by a [TransactionBuilder].
144  pub fn publish_did_document(&self, document: IotaDocument) -> TransactionBuilder<PublishDidDocument> {
145    TransactionBuilder::new(PublishDidDocument::new(document, self.sender_address()))
146  }
147
148  // TODO: define what happens for (legacy|migrated|new) documents
149  /// Updates a DID Document.
150  pub async fn publish_did_document_update(
151    &self,
152    document: IotaDocument,
153    gas_budget: u64,
154  ) -> Result<IotaDocument, Error> {
155    let mut oci =
156      if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(document.id())?).await? {
157        value
158      } else {
159        return Err(Error::Identity("only new identities can be updated".to_string()));
160      };
161
162    let controller_token = oci.get_controller_token(self).await?.ok_or_else(|| {
163      Error::Identity(format!(
164        "address {} has no control over Identity {}",
165        self.sender_address(),
166        oci.id()
167      ))
168    })?;
169
170    oci
171      .update_did_document(document.clone(), &controller_token)
172      .finish(self)
173      .await?
174      .with_gas_budget(gas_budget)
175      .build_and_execute(self)
176      .await
177      .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?;
178
179    Ok(document)
180  }
181
182  /// Deactivates a DID document.
183  pub async fn deactivate_did_output(&self, did: &IotaDID, gas_budget: u64) -> Result<(), Error> {
184    let mut oci = if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(did)?).await? {
185      value
186    } else {
187      return Err(Error::Identity("only new identities can be deactivated".to_string()));
188    };
189
190    let controller_token = oci.get_controller_token(self).await?.ok_or_else(|| {
191      Error::Identity(format!(
192        "address {} has no control over Identity {}",
193        self.sender_address(),
194        oci.id()
195      ))
196    })?;
197
198    oci
199      .deactivate_did(&controller_token)
200      .finish(self)
201      .await?
202      .with_gas_budget(gas_budget)
203      .build_and_execute(self)
204      .await
205      .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?;
206
207    Ok(())
208  }
209
210  /// Query the objects owned by the address wrapped by this client to find the object of type `tag`
211  /// and that satisfies `predicate`.
212  pub async fn find_owned_ref<P>(&self, tag: StructTag, predicate: P) -> Result<Option<ObjectRef>, Error>
213  where
214    P: Fn(&IotaObjectData) -> bool,
215  {
216    let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag));
217
218    let mut cursor = None;
219    loop {
220      let mut page = self
221        .read_api()
222        .get_owned_objects(self.sender_address(), Some(filter.clone()), cursor, None)
223        .await?;
224      let obj_ref = std::mem::take(&mut page.data)
225        .into_iter()
226        .filter_map(|res| res.data)
227        .find(|obj| predicate(obj))
228        .map(|obj_data| obj_data.object_ref());
229      cursor = page.next_cursor;
230
231      if obj_ref.is_some() {
232        return Ok(obj_ref);
233      }
234      if !page.has_next_page {
235        break;
236      }
237    }
238
239    Ok(None)
240  }
241}
242
243#[cfg_attr(feature = "send-sync", async_trait)]
244#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
245impl<S> CoreClientReadOnly for IdentityClient<S>
246where
247  S: OptionalSync,
248{
249  fn client_adapter(&self) -> &IotaClientAdapter {
250    &self.read_client
251  }
252
253  fn package_id(&self) -> ObjectID {
254    self.read_client.package_id()
255  }
256
257  fn package_history(&self) -> Vec<ObjectID> {
258    self.read_client.package_history()
259  }
260
261  fn network_name(&self) -> &NetworkName {
262    self.read_client.network()
263  }
264}
265
266impl<S> CoreClient<S> for IdentityClient<S>
267where
268  S: Signer<IotaKeySignature> + OptionalSync,
269{
270  fn sender_address(&self) -> IotaAddress {
271    IotaAddress::from(&self.public_key)
272  }
273
274  fn signer(&self) -> &S {
275    &self.signer
276  }
277
278  fn sender_public_key(&self) -> &PublicKey {
279    &self.public_key
280  }
281}
282
283/// Utility function that returns the key's bytes of a JWK encoded public ed25519 key.
284pub fn get_sender_public_key(sender_public_jwk: &Jwk) -> Result<Vec<u8>, Error> {
285  let public_key_base_64 = &sender_public_jwk
286    .try_okp_params()
287    .map_err(|err| Error::InvalidKey(format!("key not of type `Okp`; {err}")))?
288    .x;
289
290  identity_jose::jwu::decode_b64(public_key_base_64)
291    .map_err(|err| Error::InvalidKey(format!("could not decode base64 public key; {err}")))
292}
293
294/// Publishes a new DID Document on-chain. An [`crate::rebased::migration::OnChainIdentity`] will be created to contain
295/// the provided document.
296#[derive(Debug, Clone)]
297pub struct PublishDidDocument {
298  did_document: IotaDocument,
299  controller: IotaAddress,
300  cached_ptb: OnceCell<ProgrammableTransaction>,
301}
302
303impl PublishDidDocument {
304  /// Creates a new [PublishDidDocument] transaction.
305  pub fn new(did_document: IotaDocument, controller: IotaAddress) -> Self {
306    Self {
307      did_document,
308      controller,
309      cached_ptb: OnceCell::new(),
310    }
311  }
312
313  async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
314    let package = identity_package_id(client).await?;
315    let did_doc = StateMetadataDocument::from(self.did_document.clone())
316      .pack(StateMetadataEncoding::Json)
317      .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
318
319    let programmable_tx_bcs =
320      move_calls::identity::new_with_controllers(Some(&did_doc), [(self.controller, 1, false)], 1, package).await?;
321    Ok(bcs::from_bytes(&programmable_tx_bcs)?)
322  }
323}
324
325#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
326#[cfg_attr(feature = "send-sync", async_trait)]
327impl Transaction for PublishDidDocument {
328  type Output = IotaDocument;
329  type Error = Error;
330
331  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
332  where
333    C: CoreClientReadOnly + OptionalSync,
334  {
335    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
336  }
337
338  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
339  where
340    C: CoreClientReadOnly + OptionalSync,
341  {
342    let tx = {
343      let builder = IdentityBuilder::new(self.did_document)
344        .threshold(1)
345        .controller(self.controller, 1);
346      CreateIdentity::new(builder)
347    };
348
349    tx.apply(effects, client).await.map(IotaDocument::from)
350  }
351}