1use std::str::FromStr as _;
5
6use crate::rebased::client::IdentityClientReadOnly;
7use crate::rebased::iota::move_calls;
8
9use crate::rebased::Error;
10use anyhow::anyhow;
11use anyhow::Context;
12use async_trait::async_trait;
13
14use iota_interaction::ident_str;
15use iota_interaction::move_types::language_storage::StructTag;
16use iota_interaction::rpc_types::IotaData as _;
17use iota_interaction::rpc_types::IotaExecutionStatus;
18use iota_interaction::rpc_types::IotaObjectDataOptions;
19use iota_interaction::rpc_types::IotaTransactionBlockEffects;
20use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
21use iota_interaction::types::base_types::IotaAddress;
22use iota_interaction::types::base_types::ObjectID;
23use iota_interaction::types::base_types::ObjectRef;
24use iota_interaction::types::base_types::SequenceNumber;
25use iota_interaction::types::id::UID;
26use iota_interaction::types::object::Owner;
27use iota_interaction::types::transaction::ProgrammableTransaction;
28use iota_interaction::types::TypeTag;
29use iota_interaction::IotaClientTrait;
30use iota_interaction::IotaTransactionBlockEffectsMutAPI as _;
31use iota_interaction::MoveType;
32use iota_interaction::OptionalSync;
33use product_common::core_client::CoreClientReadOnly;
34use product_common::transaction::transaction_builder::Transaction;
35use product_common::transaction::transaction_builder::TransactionBuilder;
36use serde::de::DeserializeOwned;
37use serde::Deserialize;
38use serde::Deserializer;
39use serde::Serialize;
40use tokio::sync::OnceCell;
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct AuthenticatedAsset<T> {
45 id: UID,
46 #[serde(
47 deserialize_with = "deserialize_inner",
48 bound(deserialize = "T: for<'a> Deserialize<'a>")
49 )]
50 inner: T,
51 owner: IotaAddress,
52 origin: IotaAddress,
53 mutable: bool,
54 transferable: bool,
55 deletable: bool,
56}
57
58fn deserialize_inner<'de, D, T>(deserializer: D) -> Result<T, D::Error>
59where
60 D: Deserializer<'de>,
61 T: for<'a> Deserialize<'a>,
62{
63 use serde::de::Error as _;
64
65 match std::any::type_name::<T>() {
66 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => {
67 String::deserialize(deserializer).and_then(|s| serde_json::from_str(&s).map_err(D::Error::custom))
68 }
69 _ => T::deserialize(deserializer),
70 }
71}
72
73impl<T> AuthenticatedAsset<T>
74where
75 T: DeserializeOwned,
76{
77 pub async fn get_by_id(id: ObjectID, client: &impl CoreClientReadOnly) -> Result<Self, Error> {
79 let res = client
80 .client_adapter()
81 .read_api()
82 .get_object_with_options(id, IotaObjectDataOptions::new().with_content())
83 .await?;
84 let Some(data) = res.data else {
85 return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string())));
86 };
87 data
88 .content
89 .ok_or_else(|| anyhow!("No content for object with ID {id}"))
90 .and_then(|content| content.try_into_move().context("not a Move object"))
91 .and_then(|obj_data| {
92 serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object")
93 })
94 .map_err(|e| Error::ObjectLookup(e.to_string()))
95 }
96}
97
98impl<T: MoveType + Send + Sync> AuthenticatedAsset<T> {
99 async fn object_ref(&self, client: &impl CoreClientReadOnly) -> Result<ObjectRef, Error> {
100 client
101 .client_adapter()
102 .read_api()
103 .get_object_with_options(self.id(), IotaObjectDataOptions::default())
104 .await?
105 .object_ref_if_exists()
106 .ok_or_else(|| Error::ObjectLookup("missing object reference in response".to_owned()))
107 }
108
109 pub fn id(&self) -> ObjectID {
111 *self.id.object_id()
112 }
113
114 pub fn content(&self) -> &T {
116 &self.inner
117 }
118
119 pub fn transfer(
126 self,
127 recipient: IotaAddress,
128 client: &IdentityClientReadOnly,
129 ) -> Result<TransactionBuilder<TransferAsset<T>>, Error> {
130 if !self.transferable {
131 return Err(Error::InvalidConfig(format!(
132 "`AuthenticatedAsset` {} is not transferable",
133 self.id()
134 )));
135 }
136 Ok(TransactionBuilder::new(TransferAsset::new(self, recipient, client)))
137 }
138
139 pub fn delete(self, client: &IdentityClientReadOnly) -> Result<TransactionBuilder<DeleteAsset<T>>, Error> {
146 if !self.deletable {
147 return Err(Error::InvalidConfig(format!(
148 "`AuthenticatedAsset` {} cannot be deleted",
149 self.id()
150 )));
151 }
152
153 Ok(TransactionBuilder::new(DeleteAsset::new(self, client)))
154 }
155
156 pub fn set_content(
163 &mut self,
164 new_content: T,
165 client: &IdentityClientReadOnly,
166 ) -> Result<TransactionBuilder<UpdateContent<'_, T>>, Error> {
167 if !self.mutable {
168 return Err(Error::InvalidConfig(format!(
169 "`AuthenticatedAsset` {} is immutable",
170 self.id()
171 )));
172 }
173
174 Ok(TransactionBuilder::new(UpdateContent::new(self, new_content, client)))
175 }
176}
177
178#[derive(Debug)]
180pub struct AuthenticatedAssetBuilder<T> {
181 inner: T,
182 mutable: bool,
183 transferable: bool,
184 deletable: bool,
185}
186
187impl<T: MoveType> MoveType for AuthenticatedAsset<T> {
188 fn move_type(package: ObjectID) -> TypeTag {
189 TypeTag::Struct(Box::new(StructTag {
190 address: package.into(),
191 module: ident_str!("asset").into(),
192 name: ident_str!("AuthenticatedAsset").into(),
193 type_params: vec![T::move_type(package)],
194 }))
195 }
196}
197
198impl<T> AuthenticatedAssetBuilder<T>
199where
200 T: MoveType + Send + Sync + DeserializeOwned + PartialEq,
201{
202 pub fn new(content: T) -> Self {
204 Self {
205 inner: content,
206 mutable: false,
207 transferable: false,
208 deletable: false,
209 }
210 }
211
212 pub fn mutable(mut self, mutable: bool) -> Self {
216 self.mutable = mutable;
217 self
218 }
219
220 pub fn transferable(mut self, transferable: bool) -> Self {
224 self.transferable = transferable;
225 self
226 }
227
228 pub fn deletable(mut self, deletable: bool) -> Self {
232 self.deletable = deletable;
233 self
234 }
235
236 pub fn finish(self, client: &IdentityClientReadOnly) -> TransactionBuilder<CreateAsset<T>> {
238 TransactionBuilder::new(CreateAsset::new(self, client))
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct TransferProposal {
256 id: UID,
257 asset_id: ObjectID,
258 sender_cap_id: ObjectID,
259 sender_address: IotaAddress,
260 recipient_cap_id: ObjectID,
261 recipient_address: IotaAddress,
262 done: bool,
263}
264
265impl MoveType for TransferProposal {
266 fn move_type(package: ObjectID) -> TypeTag {
267 TypeTag::Struct(Box::new(StructTag {
268 address: package.into(),
269 module: ident_str!("asset").into(),
270 name: ident_str!("TransferProposal").into(),
271 type_params: vec![],
272 }))
273 }
274}
275
276impl TransferProposal {
277 pub async fn get_by_id(id: ObjectID, client: &impl CoreClientReadOnly) -> Result<Self, Error> {
279 let res = client
280 .client_adapter()
281 .read_api()
282 .get_object_with_options(id, IotaObjectDataOptions::new().with_content())
283 .await?;
284 let Some(data) = res.data else {
285 return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string())));
286 };
287 data
288 .content
289 .ok_or_else(|| anyhow!("No content for object with ID {id}"))
290 .and_then(|content| content.try_into_move().context("not a Move object"))
291 .and_then(|obj_data| {
292 serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object")
293 })
294 .map_err(|e| Error::ObjectLookup(e.to_string()))
295 }
296
297 async fn get_cap<C>(&self, cap_type: &str, client: &C) -> Result<ObjectRef, Error>
298 where
299 C: CoreClientReadOnly + OptionalSync,
300 {
301 let cap_tag = StructTag::from_str(&format!("{}::asset::{cap_type}", client.package_id()))
302 .map_err(|e| Error::ParsingFailed(e.to_string()))?;
303 let owner_address = match cap_type {
304 "SenderCap" => self.sender_address,
305 "RecipientCap" => self.recipient_address,
306 _ => unreachable!(),
307 };
308 client
309 .find_owned_ref_for_address(owner_address, cap_tag, |obj_data| {
310 cap_type == "SenderCap" && self.sender_cap_id == obj_data.object_id
311 || cap_type == "RecipientCap" && self.recipient_cap_id == obj_data.object_id
312 })
313 .await?
314 .ok_or_else(|| {
315 Error::MissingPermission(format!(
316 "no owned `{cap_type}` for transfer proposal {}",
317 self.id.object_id(),
318 ))
319 })
320 }
321
322 async fn asset_metadata(&self, client: &impl CoreClientReadOnly) -> anyhow::Result<(ObjectRef, TypeTag)> {
323 let res = client
324 .client_adapter()
325 .read_api()
326 .get_object_with_options(self.asset_id, IotaObjectDataOptions::default().with_type())
327 .await?;
328 let asset_ref = res
329 .object_ref_if_exists()
330 .context("missing object reference in response")?;
331 let param_type = res
332 .data
333 .context("missing data")
334 .and_then(|data| data.type_.context("missing type"))
335 .and_then(StructTag::try_from)
336 .and_then(|mut tag| {
337 if tag.type_params.is_empty() {
338 anyhow::bail!("no type parameter")
339 } else {
340 Ok(tag.type_params.remove(0))
341 }
342 })?;
343
344 Ok((asset_ref, param_type))
345 }
346
347 async fn initial_shared_version(&self, client: &impl CoreClientReadOnly) -> anyhow::Result<SequenceNumber> {
348 let owner = client
349 .client_adapter()
350 .read_api()
351 .get_object_with_options(*self.id.object_id(), IotaObjectDataOptions::default().with_owner())
352 .await?
353 .owner()
354 .context("missing owner information")?;
355 match owner {
356 Owner::Shared { initial_shared_version } => Ok(initial_shared_version),
357 _ => anyhow::bail!("`TransferProposal` is not a shared object"),
358 }
359 }
360
361 pub fn accept(self, client: &IdentityClientReadOnly) -> TransactionBuilder<AcceptTransfer> {
365 TransactionBuilder::new(AcceptTransfer::new(self, client))
366 }
367
368 pub fn conclude_or_cancel(self, client: &IdentityClientReadOnly) -> TransactionBuilder<ConcludeTransfer> {
374 TransactionBuilder::new(ConcludeTransfer::new(self, client))
375 }
376
377 pub fn id(&self) -> ObjectID {
379 *self.id.object_id()
380 }
381
382 pub fn sender(&self) -> IotaAddress {
384 self.sender_address
385 }
386
387 pub fn recipient(&self) -> IotaAddress {
389 self.recipient_address
390 }
391
392 pub fn is_concluded(&self) -> bool {
394 self.done
395 }
396}
397
398#[derive(Debug)]
400pub struct UpdateContent<'a, T> {
401 asset: &'a mut AuthenticatedAsset<T>,
402 new_content: T,
403 cached_ptb: OnceCell<ProgrammableTransaction>,
404 package: ObjectID,
405}
406
407impl<'a, T: MoveType + Send + Sync> UpdateContent<'a, T> {
408 pub fn new(asset: &'a mut AuthenticatedAsset<T>, new_content: T, client: &IdentityClientReadOnly) -> Self {
410 Self {
411 asset,
412 new_content,
413 cached_ptb: OnceCell::new(),
414 package: client.package_id(),
415 }
416 }
417
418 async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
419 let tx_bcs = move_calls::asset::update(self.asset.object_ref(client).await?, &self.new_content, self.package)?;
420
421 Ok(bcs::from_bytes(&tx_bcs)?)
422 }
423}
424
425#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
426#[cfg_attr(feature = "send-sync", async_trait)]
427impl<T> Transaction for UpdateContent<'_, T>
428where
429 T: MoveType + Send + Sync,
430{
431 type Output = ();
432 type Error = Error;
433
434 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
435 where
436 C: CoreClientReadOnly + OptionalSync,
437 {
438 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
439 }
440 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
441 where
442 C: CoreClientReadOnly + OptionalSync,
443 {
444 if let IotaExecutionStatus::Failure { error } = effects.status() {
445 return Err(Error::TransactionUnexpectedResponse(error.clone()));
446 }
447
448 if let Some(asset_pos) = effects
449 .mutated()
450 .iter()
451 .enumerate()
452 .find(|(_, obj)| obj.object_id() == self.asset.id())
453 .map(|(i, _)| i)
454 {
455 effects.mutated_mut().swap_remove(asset_pos);
456 self.asset.inner = self.new_content;
457 }
458
459 Ok(())
460 }
461}
462
463#[derive(Debug)]
465pub struct DeleteAsset<T> {
466 asset: AuthenticatedAsset<T>,
467 cached_ptb: OnceCell<ProgrammableTransaction>,
468 package: ObjectID,
469}
470
471impl<T: MoveType + Send + Sync> DeleteAsset<T> {
472 pub fn new(asset: AuthenticatedAsset<T>, client: &IdentityClientReadOnly) -> Self {
474 Self {
475 asset,
476 cached_ptb: OnceCell::new(),
477 package: client.package_id(),
478 }
479 }
480
481 async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
482 let asset_ref = self.asset.object_ref(client).await?;
483 let tx_bcs = move_calls::asset::delete::<T>(asset_ref, self.package)?;
484
485 Ok(bcs::from_bytes(&tx_bcs)?)
486 }
487}
488
489#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
490#[cfg_attr(feature = "send-sync", async_trait)]
491impl<T> Transaction for DeleteAsset<T>
492where
493 T: MoveType + Send + Sync,
494{
495 type Output = ();
496 type Error = Error;
497
498 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
499 where
500 C: CoreClientReadOnly + OptionalSync,
501 {
502 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
503 }
504 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
505 where
506 C: CoreClientReadOnly + OptionalSync,
507 {
508 if let IotaExecutionStatus::Failure { error } = effects.status() {
509 return Err(Error::TransactionUnexpectedResponse(error.clone()));
510 }
511
512 if let Some(asset_pos) = effects
513 .deleted()
514 .iter()
515 .enumerate()
516 .find_map(|(i, obj)| (obj.object_id == self.asset.id()).then_some(i))
517 {
518 effects.deleted_mut().swap_remove(asset_pos);
519 Ok(())
520 } else {
521 Err(Error::TransactionUnexpectedResponse(format!(
522 "cannot find asset {} in the list of delete objects",
523 self.asset.id()
524 )))
525 }
526 }
527}
528#[derive(Debug)]
530pub struct CreateAsset<T> {
531 builder: AuthenticatedAssetBuilder<T>,
532 cached_ptb: OnceCell<ProgrammableTransaction>,
533 package: ObjectID,
534}
535
536impl<T: MoveType> CreateAsset<T> {
537 pub fn new(builder: AuthenticatedAssetBuilder<T>, client: &IdentityClientReadOnly) -> Self {
539 Self {
540 builder,
541 cached_ptb: OnceCell::new(),
542 package: client.package_id(),
543 }
544 }
545
546 async fn make_ptb(&self, _client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
547 let AuthenticatedAssetBuilder {
548 ref inner,
549 mutable,
550 transferable,
551 deletable,
552 } = self.builder;
553 let pt_bcs = move_calls::asset::new_asset(inner, mutable, transferable, deletable, self.package)?;
554 Ok(bcs::from_bytes(&pt_bcs)?)
555 }
556}
557
558#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
559#[cfg_attr(feature = "send-sync", async_trait)]
560impl<T> Transaction for CreateAsset<T>
561where
562 T: MoveType + DeserializeOwned + PartialEq + Send + Sync,
563{
564 type Output = AuthenticatedAsset<T>;
565 type Error = Error;
566
567 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
568 where
569 C: CoreClientReadOnly + OptionalSync,
570 {
571 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
572 }
573
574 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
575 where
576 C: CoreClientReadOnly + OptionalSync,
577 {
578 if let IotaExecutionStatus::Failure { error } = effects.status() {
579 return Err(Error::TransactionUnexpectedResponse(error.clone()));
580 }
581
582 let created_objects = effects
583 .created()
584 .iter()
585 .enumerate()
586 .filter(|(_, obj)| obj.owner.is_address_owned())
587 .map(|(i, obj)| (i, obj.object_id()));
588
589 let is_target_asset = |asset: &AuthenticatedAsset<T>| -> bool {
590 asset.inner == self.builder.inner
591 && asset.transferable == self.builder.transferable
592 && asset.mutable == self.builder.mutable
593 && asset.deletable == self.builder.deletable
594 };
595
596 let mut target_asset_pos = None;
597 let mut target_asset = None;
598 for (i, obj_id) in created_objects {
599 match AuthenticatedAsset::get_by_id(obj_id, client).await {
600 Ok(asset) if is_target_asset(&asset) => {
601 target_asset_pos = Some(i);
602 target_asset = Some(asset);
603 break;
604 }
605 _ => continue,
606 }
607 }
608
609 let (Some(pos), Some(asset)) = (target_asset_pos, target_asset) else {
610 return Err(Error::TransactionUnexpectedResponse(
611 "failed to find the asset created by this operation in transaction's effects".to_owned(),
612 ));
613 };
614
615 effects.created_mut().swap_remove(pos);
616
617 Ok(asset)
618 }
619}
620
621#[derive(Debug)]
623pub struct TransferAsset<T> {
624 asset: AuthenticatedAsset<T>,
625 recipient: IotaAddress,
626 cached_ptb: OnceCell<ProgrammableTransaction>,
627 package: ObjectID,
628}
629
630impl<T: MoveType + Send + Sync> TransferAsset<T> {
631 pub fn new(asset: AuthenticatedAsset<T>, recipient: IotaAddress, client: &IdentityClientReadOnly) -> Self {
633 Self {
634 asset,
635 recipient,
636 cached_ptb: OnceCell::new(),
637 package: client.package_id(),
638 }
639 }
640
641 async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
642 let bcs = move_calls::asset::transfer::<T>(self.asset.object_ref(client).await?, self.recipient, self.package)?;
643
644 Ok(bcs::from_bytes(&bcs)?)
645 }
646}
647
648#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
649#[cfg_attr(feature = "send-sync", async_trait)]
650impl<T> Transaction for TransferAsset<T>
651where
652 T: MoveType + Send + Sync,
653{
654 type Output = TransferProposal;
655 type Error = Error;
656
657 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
658 where
659 C: CoreClientReadOnly + OptionalSync,
660 {
661 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
662 }
663 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
664 where
665 C: CoreClientReadOnly + OptionalSync,
666 {
667 if let IotaExecutionStatus::Failure { error } = effects.status() {
668 return Err(Error::TransactionUnexpectedResponse(error.clone()));
669 }
670
671 let created_objects = effects
672 .created()
673 .iter()
674 .enumerate()
675 .filter(|(_, obj)| obj.owner.is_shared())
676 .map(|(i, obj)| (i, obj.object_id()));
677
678 let is_target_proposal = |proposal: &TransferProposal| -> bool {
679 proposal.asset_id == self.asset.id() && proposal.recipient_address == self.recipient
680 };
681
682 let mut target_proposal_pos = None;
683 let mut target_proposal = None;
684 for (i, obj_id) in created_objects {
685 match TransferProposal::get_by_id(obj_id, client).await {
686 Ok(proposal) if is_target_proposal(&proposal) => {
687 target_proposal_pos = Some(i);
688 target_proposal = Some(proposal);
689 break;
690 }
691 _ => continue,
692 }
693 }
694
695 let (Some(pos), Some(proposal)) = (target_proposal_pos, target_proposal) else {
696 return Err(Error::TransactionUnexpectedResponse(
697 "failed to find the TransferProposal created by this operation in transaction's effects".to_owned(),
698 ));
699 };
700
701 effects.created_mut().swap_remove(pos);
702
703 Ok(proposal)
704 }
705}
706
707#[derive(Debug)]
709pub struct AcceptTransfer {
710 proposal: TransferProposal,
711 cached_ptb: OnceCell<ProgrammableTransaction>,
712 package: ObjectID,
713}
714
715impl AcceptTransfer {
716 pub fn new(proposal: TransferProposal, client: &IdentityClientReadOnly) -> Self {
718 Self {
719 proposal,
720 cached_ptb: OnceCell::new(),
721 package: client.package_id(),
722 }
723 }
724
725 async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
726 where
727 C: CoreClientReadOnly + OptionalSync,
728 {
729 if self.proposal.done {
730 return Err(Error::TransactionBuildingFailed(
731 "the transfer has already been concluded".to_owned(),
732 ));
733 }
734
735 let cap = self.proposal.get_cap("RecipientCap", client).await?;
736 let (asset_ref, param_type) = self
737 .proposal
738 .asset_metadata(client)
739 .await
740 .map_err(|e| Error::ObjectLookup(e.to_string()))?;
741 let initial_shared_version = self
742 .proposal
743 .initial_shared_version(client)
744 .await
745 .map_err(|e| Error::ObjectLookup(e.to_string()))?;
746 let bcs = move_calls::asset::accept_proposal(
747 (self.proposal.id(), initial_shared_version),
748 cap,
749 asset_ref,
750 param_type,
751 self.package,
752 )?;
753
754 Ok(bcs::from_bytes(&bcs)?)
755 }
756}
757
758#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
759#[cfg_attr(feature = "send-sync", async_trait)]
760impl Transaction for AcceptTransfer {
761 type Output = ();
762 type Error = Error;
763
764 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
765 where
766 C: CoreClientReadOnly + OptionalSync,
767 {
768 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
769 }
770
771 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
772 where
773 C: CoreClientReadOnly + OptionalSync,
774 {
775 if let IotaExecutionStatus::Failure { error } = effects.status() {
776 return Err(Error::TransactionUnexpectedResponse(error.clone()));
777 }
778
779 if let Some(i) = effects
780 .deleted()
781 .iter()
782 .enumerate()
783 .find_map(|(i, obj)| (obj.object_id == self.proposal.recipient_cap_id).then_some(i))
785 {
786 effects.deleted_mut().swap_remove(i);
787 Ok(())
788 } else {
789 Err(Error::TransactionUnexpectedResponse(format!(
790 "transfer of asset {} through proposal {} wasn't successful",
791 self.proposal.asset_id,
792 self.proposal.id.object_id()
793 )))
794 }
795 }
796}
797
798#[derive(Debug)]
800pub struct ConcludeTransfer {
801 proposal: TransferProposal,
802 cached_ptb: OnceCell<ProgrammableTransaction>,
803 package: ObjectID,
804}
805
806impl ConcludeTransfer {
807 pub fn new(proposal: TransferProposal, client: &IdentityClientReadOnly) -> Self {
809 Self {
810 proposal,
811 cached_ptb: OnceCell::new(),
812 package: client.package_id(),
813 }
814 }
815
816 async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
817 where
818 C: CoreClientReadOnly + OptionalSync,
819 {
820 let cap = self.proposal.get_cap("SenderCap", client).await?;
821 let (asset_ref, param_type) = self
822 .proposal
823 .asset_metadata(client)
824 .await
825 .map_err(|e| Error::ObjectLookup(e.to_string()))?;
826 let initial_shared_version = self
827 .proposal
828 .initial_shared_version(client)
829 .await
830 .map_err(|e| Error::ObjectLookup(e.to_string()))?;
831
832 let tx_bcs = move_calls::asset::conclude_or_cancel(
833 (self.proposal.id(), initial_shared_version),
834 cap,
835 asset_ref,
836 param_type,
837 self.package,
838 )?;
839
840 Ok(bcs::from_bytes(&tx_bcs)?)
841 }
842}
843
844#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
845#[cfg_attr(feature = "send-sync", async_trait)]
846impl Transaction for ConcludeTransfer {
847 type Output = ();
848 type Error = Error;
849
850 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
851 where
852 C: CoreClientReadOnly + OptionalSync,
853 {
854 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
855 }
856
857 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Error>
858 where
859 C: CoreClientReadOnly + OptionalSync,
860 {
861 if let IotaExecutionStatus::Failure { error } = effects.status() {
862 return Err(Error::TransactionUnexpectedResponse(error.clone()));
863 }
864
865 let mut idx_to_remove = effects
866 .deleted()
867 .iter()
868 .enumerate()
869 .filter_map(|(i, obj)| {
870 (obj.object_id == *self.proposal.id.object_id() || obj.object_id == self.proposal.sender_cap_id).then_some(i)
871 })
872 .collect::<Vec<_>>();
873
874 if idx_to_remove.len() < 2 {
875 return Err(Error::TransactionUnexpectedResponse(format!(
876 "conclusion or canceling of proposal {} wasn't successful",
877 self.proposal.id.object_id()
878 )));
879 }
880
881 idx_to_remove.sort_unstable();
885 for i in idx_to_remove.into_iter().rev() {
886 effects.deleted_mut().swap_remove(i);
887 }
888
889 Ok(())
890 }
891}