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