identity_iota_core/rebased/client/
full_client.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
61pub struct KeyId(String);
62
63impl KeyId {
64 pub fn new(id: impl Into<String>) -> Self {
66 Self(id.into())
67 }
68
69 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#[derive(Debug, Clone, Copy)]
89#[non_exhaustive]
90pub struct NoSigner;
91
92#[derive(Clone)]
94pub struct IdentityClient<S = NoSigner> {
95 pub(super) read_client: IdentityClientReadOnly,
97 pub(super) public_key: Option<PublicKey>,
102 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#[derive(Debug, thiserror::Error)]
116#[error("failed to create an 'IdentityClient' from the given 'IotaClient'")]
117#[non_exhaustive]
118pub struct FromIotaClientError {
119 #[source]
121 pub kind: FromIotaClientErrorKind,
122}
123
124#[derive(Debug, thiserror::Error)]
126#[non_exhaustive]
127pub enum FromIotaClientErrorKind {
128 #[error("an IOTA Identity package ID must be supplied when connecting to an unofficial IOTA network")]
130 MissingPackageId,
131 #[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 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 #[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 pub fn public_key(&self) -> &PublicKey {
205 self.public_key.as_ref().expect("public_key is set")
206 }
207
208 #[inline(always)]
210 pub fn address(&self) -> IotaAddress {
211 IotaAddress::from(self.public_key())
212 }
213
214 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 pub fn create_identity(&self, iota_document: IotaDocument) -> IdentityBuilder {
223 IdentityBuilder::new(iota_document)
224 }
225
226 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 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 pub fn publish_did_document(&self, document: IotaDocument) -> TransactionBuilder<PublishDidDocument> {
255 TransactionBuilder::new(PublishDidDocument::new(document, self.sender_address()))
256 }
257
258 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 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 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 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
458pub 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#[derive(Debug, Clone)]
472pub struct PublishDidDocument {
473 did_document: IotaDocument,
474 controller: IotaAddress,
475 cached_ptb: OnceCell<ProgrammableTransaction>,
476}
477
478impl PublishDidDocument {
479 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#[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#[derive(Debug, thiserror::Error)]
575#[error("failed to prepare transaction to update DID '{}'", did_document.id())]
576#[non_exhaustive]
577pub struct MakeUpdateDidDocTxError {
578 pub did_document: IotaDocument,
580 pub kind: MakeUpdateDidDocTxErrorKind,
582}
583
584#[derive(Debug, thiserror::Error)]
586#[non_exhaustive]
587pub enum MakeUpdateDidDocTxErrorKind {
588 #[error(transparent)]
590 RpcError(Box<dyn std::error::Error + Send + Sync>),
591 #[error(transparent)]
593 IdentityResolution(#[from] IdentityResolutionError),
594 #[error(transparent)]
596 NotAController(#[from] NotAController),
597 #[error("Identity's DID Document is deleted")]
599 DeletedIdentityDocument,
600 #[error(transparent)]
603 InsufficientVotingPower(#[from] InsufficientControllerVotingPower),
604}