1use std::collections::HashMap;
5use std::ops::Not;
6use std::str::FromStr;
7use std::vec;
8
9use anyhow::anyhow;
10use move_core_types::ident_str;
11use move_core_types::language_storage::{ModuleId, StructTag};
12use move_core_types::resolver::ModuleResolver;
13use serde::Deserialize;
14use serde::Serialize;
15
16use sui_json_rpc_types::SuiProgrammableMoveCall;
17use sui_json_rpc_types::SuiProgrammableTransactionBlock;
18use sui_json_rpc_types::{BalanceChange, SuiArgument};
19use sui_json_rpc_types::{SuiCallArg, SuiCommand};
20use sui_sdk::rpc_types::{
21 SuiTransactionBlockData, SuiTransactionBlockDataAPI, SuiTransactionBlockEffectsAPI,
22 SuiTransactionBlockKind, SuiTransactionBlockResponse,
23};
24use sui_types::base_types::{ObjectID, SequenceNumber, SuiAddress};
25use sui_types::gas_coin::GasCoin;
26use sui_types::governance::{ADD_STAKE_FUN_NAME, WITHDRAW_STAKE_FUN_NAME};
27use sui_types::object::Owner;
28use sui_types::sui_system_state::SUI_SYSTEM_MODULE_NAME;
29use sui_types::transaction::TransactionData;
30use sui_types::{SUI_SYSTEM_ADDRESS, SUI_SYSTEM_PACKAGE_ID};
31
32use crate::types::internal_operation::{PayCoin, PaySui, Stake, WithdrawStake};
33use crate::types::{
34 AccountIdentifier, Amount, CoinAction, CoinChange, CoinID, CoinIdentifier, Currency,
35 InternalOperation, OperationIdentifier, OperationStatus, OperationType,
36};
37use crate::{CoinMetadataCache, Error, SUI};
38
39#[cfg(test)]
40#[path = "unit_tests/operations_tests.rs"]
41mod operations_tests;
42
43#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
44pub struct Operations(Vec<Operation>);
45
46impl FromIterator<Operation> for Operations {
47 fn from_iter<T: IntoIterator<Item = Operation>>(iter: T) -> Self {
48 Operations::new(iter.into_iter().collect())
49 }
50}
51
52impl FromIterator<Vec<Operation>> for Operations {
53 fn from_iter<T: IntoIterator<Item = Vec<Operation>>>(iter: T) -> Self {
54 iter.into_iter().flatten().collect()
55 }
56}
57
58impl IntoIterator for Operations {
59 type Item = Operation;
60 type IntoIter = vec::IntoIter<Operation>;
61 fn into_iter(self) -> Self::IntoIter {
62 self.0.into_iter()
63 }
64}
65
66impl Operations {
67 pub fn new(mut ops: Vec<Operation>) -> Self {
68 for (index, op) in ops.iter_mut().enumerate() {
69 op.operation_identifier = (index as u64).into()
70 }
71 Self(ops)
72 }
73
74 pub fn contains(&self, other: &Operations) -> bool {
75 for (i, other_op) in other.0.iter().enumerate() {
76 if let Some(op) = self.0.get(i) {
77 if op != other_op {
78 return false;
79 }
80 } else {
81 return false;
82 }
83 }
84 true
85 }
86
87 pub fn set_status(mut self, status: Option<OperationStatus>) -> Self {
88 for op in &mut self.0 {
89 op.status = status
90 }
91 self
92 }
93
94 pub fn type_(&self) -> Option<OperationType> {
95 self.0.first().map(|op| op.type_)
96 }
97
98 pub fn into_internal(self) -> Result<InternalOperation, Error> {
100 let type_ = self
101 .type_()
102 .ok_or_else(|| Error::MissingInput("Operation type".into()))?;
103 match type_ {
104 OperationType::PaySui => self.pay_sui_ops_to_internal(),
105 OperationType::PayCoin => self.pay_coin_ops_to_internal(),
106 OperationType::Stake => self.stake_ops_to_internal(),
107 OperationType::WithdrawStake => self.withdraw_stake_ops_to_internal(),
108 op => Err(Error::UnsupportedOperation(op)),
109 }
110 }
111
112 fn pay_sui_ops_to_internal(self) -> Result<InternalOperation, Error> {
113 let mut recipients = vec![];
114 let mut amounts = vec![];
115 let mut sender = None;
116 for op in self {
117 if let (Some(amount), Some(account)) = (op.amount.clone(), op.account.clone()) {
118 if amount.value.is_negative() {
119 sender = Some(account.address)
120 } else {
121 recipients.push(account.address);
122 let amount = amount.value.abs();
123 if amount > u64::MAX as i128 {
124 return Err(Error::InvalidInput(
125 "Input amount exceed u64::MAX".to_string(),
126 ));
127 }
128 amounts.push(amount as u64)
129 }
130 }
131 }
132 let sender = sender.ok_or_else(|| Error::MissingInput("Sender address".to_string()))?;
133 Ok(InternalOperation::PaySui(PaySui {
134 sender,
135 recipients,
136 amounts,
137 }))
138 }
139
140 fn pay_coin_ops_to_internal(self) -> Result<InternalOperation, Error> {
141 let mut recipients = vec![];
142 let mut amounts = vec![];
143 let mut sender = None;
144 let mut currency = None;
145 for op in self {
146 if let (Some(amount), Some(account)) = (op.amount.clone(), op.account.clone()) {
147 currency = currency.or(Some(amount.currency));
148 if amount.value.is_negative() {
149 sender = Some(account.address)
150 } else {
151 recipients.push(account.address);
152 let amount = amount.value.abs();
153 if amount > u64::MAX as i128 {
154 return Err(Error::InvalidInput(
155 "Input amount exceed u64::MAX".to_string(),
156 ));
157 }
158 amounts.push(amount as u64)
159 }
160 }
161 }
162 let sender = sender.ok_or_else(|| Error::MissingInput("Sender address".to_string()))?;
163 let currency = currency.ok_or_else(|| Error::MissingInput("Currency".to_string()))?;
164 Ok(InternalOperation::PayCoin(PayCoin {
165 sender,
166 recipients,
167 amounts,
168 currency,
169 }))
170 }
171
172 fn stake_ops_to_internal(self) -> Result<InternalOperation, Error> {
173 let mut ops = self
174 .0
175 .into_iter()
176 .filter(|op| op.type_ == OperationType::Stake)
177 .collect::<Vec<_>>();
178 if ops.len() != 1 {
179 return Err(Error::MalformedOperationError(
180 "Delegation should only have one operation.".into(),
181 ));
182 }
183 let op = ops.pop().unwrap();
185 let sender = op
186 .account
187 .ok_or_else(|| Error::MissingInput("Sender address".to_string()))?
188 .address;
189 let metadata = op
190 .metadata
191 .ok_or_else(|| Error::MissingInput("Stake metadata".to_string()))?;
192
193 let amount = if let Some(amount) = op.amount {
195 if amount.value.is_positive() {
196 return Err(Error::MalformedOperationError(
197 "Stake amount should be negative.".into(),
198 ));
199 }
200 Some(amount.value.unsigned_abs() as u64)
201 } else {
202 None
203 };
204
205 let OperationMetadata::Stake { validator } = metadata else {
206 return Err(Error::InvalidInput(
207 "Cannot find delegation info from metadata.".into(),
208 ));
209 };
210
211 Ok(InternalOperation::Stake(Stake {
212 sender,
213 validator,
214 amount,
215 }))
216 }
217
218 fn withdraw_stake_ops_to_internal(self) -> Result<InternalOperation, Error> {
219 let mut ops = self
220 .0
221 .into_iter()
222 .filter(|op| op.type_ == OperationType::WithdrawStake)
223 .collect::<Vec<_>>();
224 if ops.len() != 1 {
225 return Err(Error::MalformedOperationError(
226 "Delegation should only have one operation.".into(),
227 ));
228 }
229 let op = ops.pop().unwrap();
231 let sender = op
232 .account
233 .ok_or_else(|| Error::MissingInput("Sender address".to_string()))?
234 .address;
235
236 let stake_ids = if let Some(metadata) = op.metadata {
237 let OperationMetadata::WithdrawStake { stake_ids } = metadata else {
238 return Err(Error::InvalidInput(
239 "Cannot find withdraw stake info from metadata.".into(),
240 ));
241 };
242 stake_ids
243 } else {
244 vec![]
245 };
246
247 Ok(InternalOperation::WithdrawStake(WithdrawStake {
248 sender,
249 stake_ids,
250 }))
251 }
252
253 fn from_transaction(
254 tx: SuiTransactionBlockKind,
255 sender: SuiAddress,
256 status: Option<OperationStatus>,
257 ) -> Result<Vec<Operation>, Error> {
258 Ok(match tx {
259 SuiTransactionBlockKind::ProgrammableTransaction(pt)
260 if status != Some(OperationStatus::Failure) =>
261 {
262 Self::parse_programmable_transaction(sender, status, pt)?
263 }
264 _ => vec![Operation::generic_op(status, sender, tx)],
265 })
266 }
267
268 fn parse_programmable_transaction(
269 sender: SuiAddress,
270 status: Option<OperationStatus>,
271 pt: SuiProgrammableTransactionBlock,
272 ) -> Result<Vec<Operation>, Error> {
273 #[derive(Debug)]
274 enum KnownValue {
275 GasCoin(u64),
276 }
277 fn resolve_result(
278 known_results: &[Vec<KnownValue>],
279 i: u16,
280 j: u16,
281 ) -> Option<&KnownValue> {
282 known_results
283 .get(i as usize)
284 .and_then(|inner| inner.get(j as usize))
285 }
286 fn split_coins(
287 inputs: &[SuiCallArg],
288 known_results: &[Vec<KnownValue>],
289 coin: SuiArgument,
290 amounts: &[SuiArgument],
291 ) -> Option<Vec<KnownValue>> {
292 match coin {
293 SuiArgument::Result(i) => {
294 let KnownValue::GasCoin(_) = resolve_result(known_results, i, 0)?;
295 }
296 SuiArgument::NestedResult(i, j) => {
297 let KnownValue::GasCoin(_) = resolve_result(known_results, i, j)?;
298 }
299 SuiArgument::GasCoin => (),
300 SuiArgument::Input(_) => (),
302 };
303 let amounts = amounts
304 .iter()
305 .map(|amount| {
306 let value: u64 = match *amount {
307 SuiArgument::Input(i) => {
308 u64::from_str(inputs.get(i as usize)?.pure()?.to_json_value().as_str()?)
309 .ok()?
310 }
311 SuiArgument::GasCoin
312 | SuiArgument::Result(_)
313 | SuiArgument::NestedResult(_, _) => return None,
314 };
315 Some(KnownValue::GasCoin(value))
316 })
317 .collect::<Option<_>>()?;
318 Some(amounts)
319 }
320 fn transfer_object(
321 aggregated_recipients: &mut HashMap<SuiAddress, u64>,
322 inputs: &[SuiCallArg],
323 known_results: &[Vec<KnownValue>],
324 objs: &[SuiArgument],
325 recipient: SuiArgument,
326 ) -> Option<Vec<KnownValue>> {
327 let addr = match recipient {
328 SuiArgument::Input(i) => inputs.get(i as usize)?.pure()?.to_sui_address().ok()?,
329 SuiArgument::GasCoin | SuiArgument::Result(_) | SuiArgument::NestedResult(_, _) => {
330 return None;
331 }
332 };
333 for obj in objs {
334 let value = match *obj {
335 SuiArgument::Result(i) => {
336 let KnownValue::GasCoin(value) = resolve_result(known_results, i, 0)?;
337 value
338 }
339 SuiArgument::NestedResult(i, j) => {
340 let KnownValue::GasCoin(value) = resolve_result(known_results, i, j)?;
341 value
342 }
343 SuiArgument::GasCoin | SuiArgument::Input(_) => return None,
344 };
345 let aggregate = aggregated_recipients.entry(addr).or_default();
346 *aggregate += value;
347 }
348 Some(vec![])
349 }
350 fn stake_call(
351 inputs: &[SuiCallArg],
352 known_results: &[Vec<KnownValue>],
353 call: &SuiProgrammableMoveCall,
354 ) -> Result<Option<(Option<u64>, SuiAddress)>, Error> {
355 let SuiProgrammableMoveCall { arguments, .. } = call;
356 let (amount, validator) = match &arguments[..] {
357 [_, coin, validator] => {
358 let amount = match coin {
359 SuiArgument::Result(i) => {
360 let KnownValue::GasCoin(value) = resolve_result(known_results, *i, 0)
361 .ok_or_else(|| {
362 anyhow!("Cannot resolve Gas coin value at Result({i})")
363 })?;
364 value
365 }
366 _ => return Ok(None),
367 };
368 let (some_amount, validator) = match validator {
369 SuiArgument::Input(i) => (
374 *i == 1,
375 inputs
376 .get(*i as usize)
377 .and_then(|input| input.pure())
378 .map(|v| v.to_sui_address())
379 .transpose(),
380 ),
381 _ => return Ok(None),
382 };
383 (some_amount.then_some(*amount), validator)
384 }
385 _ => Err(anyhow!(
386 "Error encountered when extracting arguments from move call, expecting 3 elements, got {}",
387 arguments.len()
388 ))?,
389 };
390 Ok(validator.map(|v| v.map(|v| (amount, v)))?)
391 }
392
393 fn unstake_call(
394 inputs: &[SuiCallArg],
395 call: &SuiProgrammableMoveCall,
396 ) -> Result<Option<ObjectID>, Error> {
397 let SuiProgrammableMoveCall { arguments, .. } = call;
398 let id = match &arguments[..] {
399 [_, stake_id] => {
400 match stake_id {
401 SuiArgument::Input(i) => {
402 let id = inputs
403 .get(*i as usize)
404 .and_then(|input| input.object())
405 .ok_or_else(|| anyhow!("Cannot find stake id from input args."))?;
406 let some_id = i % 2 == 1;
409 some_id.then_some(id)
410 }
411 _ => return Ok(None),
412 }
413 }
414 _ => Err(anyhow!(
415 "Error encountered when extracting arguments from move call, expecting 3 elements, got {}",
416 arguments.len()
417 ))?,
418 };
419 Ok(id.cloned())
420 }
421 let SuiProgrammableTransactionBlock { inputs, commands } = &pt;
422 let mut known_results: Vec<Vec<KnownValue>> = vec![];
423 let mut aggregated_recipients: HashMap<SuiAddress, u64> = HashMap::new();
424 let mut needs_generic = false;
425 let mut operations = vec![];
426 let mut stake_ids = vec![];
427 let mut currency: Option<Currency> = None;
428
429 for command in commands {
430 let result = match command {
431 SuiCommand::SplitCoins(coin, amounts) => {
432 split_coins(inputs, &known_results, *coin, amounts)
433 }
434 SuiCommand::TransferObjects(objs, addr) => transfer_object(
435 &mut aggregated_recipients,
436 inputs,
437 &known_results,
438 objs,
439 *addr,
440 ),
441 SuiCommand::MoveCall(m) if Self::is_stake_call(m) => {
442 stake_call(inputs, &known_results, m)?.map(|(amount, validator)| {
443 let amount = amount.map(|amount| Amount::new(-(amount as i128), None));
444 operations.push(Operation {
445 operation_identifier: Default::default(),
446 type_: OperationType::Stake,
447 status,
448 account: Some(sender.into()),
449 amount,
450 coin_change: None,
451 metadata: Some(OperationMetadata::Stake { validator }),
452 });
453 vec![]
454 })
455 }
456 SuiCommand::MoveCall(m) if Self::is_unstake_call(m) => {
457 let stake_id = unstake_call(inputs, m)?;
458 stake_ids.push(stake_id);
459 Some(vec![])
460 }
461 SuiCommand::MergeCoins(_merge_into, _merges) => {
462 Some(vec![])
464 }
465 _ => None,
466 };
467 if let Some(result) = result {
468 known_results.push(result)
469 } else {
470 needs_generic = true;
471 break;
472 }
473 }
474
475 if !needs_generic && !aggregated_recipients.is_empty() {
476 let total_paid: u64 = aggregated_recipients.values().copied().sum();
477 operations.extend(
478 aggregated_recipients
479 .into_iter()
480 .map(|(recipient, amount)| {
481 currency = inputs.iter().last().and_then(|arg| {
482 if let SuiCallArg::Pure(value) = arg {
483 let bytes = value
484 .value()
485 .to_json_value()
486 .as_array()?
487 .clone()
488 .into_iter()
489 .map(|v| v.as_u64().map(|n| n as u8))
490 .collect::<Option<Vec<u8>>>()?;
491 bcs::from_bytes::<String>(&bytes)
492 .ok()
493 .and_then(|bcs_str| serde_json::from_str(&bcs_str).ok())
494 } else {
495 None
496 }
497 });
498 match currency {
499 Some(_) => Operation::pay_coin(
500 status,
501 recipient,
502 amount.into(),
503 currency.clone(),
504 ),
505 None => Operation::pay_sui(status, recipient, amount.into()),
506 }
507 }),
508 );
509 match currency {
510 Some(_) => operations.push(Operation::pay_coin(
511 status,
512 sender,
513 -(total_paid as i128),
514 currency.clone(),
515 )),
516 _ => operations.push(Operation::pay_sui(status, sender, -(total_paid as i128))),
517 }
518 } else if !stake_ids.is_empty() {
519 let stake_ids = stake_ids.into_iter().flatten().collect::<Vec<_>>();
520 let metadata = stake_ids
521 .is_empty()
522 .not()
523 .then_some(OperationMetadata::WithdrawStake { stake_ids });
524 operations.push(Operation {
525 operation_identifier: Default::default(),
526 type_: OperationType::WithdrawStake,
527 status,
528 account: Some(sender.into()),
529 amount: None,
530 coin_change: None,
531 metadata,
532 });
533 } else if operations.is_empty() {
534 operations.push(Operation::generic_op(
535 status,
536 sender,
537 SuiTransactionBlockKind::ProgrammableTransaction(pt),
538 ))
539 }
540 Ok(operations)
541 }
542
543 fn is_stake_call(tx: &SuiProgrammableMoveCall) -> bool {
544 tx.package == SUI_SYSTEM_PACKAGE_ID
545 && tx.module == SUI_SYSTEM_MODULE_NAME.as_str()
546 && tx.function == ADD_STAKE_FUN_NAME.as_str()
547 }
548
549 fn is_unstake_call(tx: &SuiProgrammableMoveCall) -> bool {
550 tx.package == SUI_SYSTEM_PACKAGE_ID
551 && tx.module == SUI_SYSTEM_MODULE_NAME.as_str()
552 && tx.function == WITHDRAW_STAKE_FUN_NAME.as_str()
553 }
554
555 fn process_balance_change(
556 gas_owner: SuiAddress,
557 gas_used: i128,
558 balance_changes: &[(BalanceChange, Currency)],
559 status: Option<OperationStatus>,
560 balances: HashMap<(SuiAddress, Currency), i128>,
561 ) -> impl Iterator<Item = Operation> {
562 let mut balances =
563 balance_changes
564 .iter()
565 .fold(balances, |mut balances, (balance_change, ccy)| {
566 if let Owner::AddressOwner(owner) = balance_change.owner {
568 *balances.entry((owner, ccy.clone())).or_default() += balance_change.amount;
569 }
570 balances
571 });
572 *balances.entry((gas_owner, SUI.clone())).or_default() -= gas_used;
574
575 let balance_change = balances.into_iter().filter(|(_, amount)| *amount != 0).map(
576 move |((addr, currency), amount)| {
577 Operation::balance_change(status, addr, amount, currency)
578 },
579 );
580
581 let gas = if gas_used != 0 {
582 vec![Operation::gas(gas_owner, gas_used)]
583 } else {
584 vec![]
586 };
587 balance_change.chain(gas)
588 }
589
590 fn is_gascoin_transfer(tx: &SuiTransactionBlockKind) -> bool {
592 if let SuiTransactionBlockKind::ProgrammableTransaction(pt) = tx {
593 let SuiProgrammableTransactionBlock {
594 inputs: _,
595 commands,
596 } = &pt;
597 return commands.iter().any(|command| match command {
598 SuiCommand::TransferObjects(objs, _) => objs.contains(&SuiArgument::GasCoin),
599 _ => false,
600 });
601 }
602 false
603 }
604
605 fn add_missing_gas_owner(operations: &mut Vec<Operation>, gas_owner: SuiAddress) {
608 if !operations.iter().any(|operation| {
609 if let Some(amount) = &operation.amount
610 && let Some(account) = &operation.account
611 && account.address == gas_owner
612 && amount.currency == *SUI
613 {
614 return true;
615 }
616 false
617 }) {
618 operations.push(Operation::balance_change(
619 Some(OperationStatus::Success),
620 gas_owner,
621 0,
622 SUI.clone(),
623 ));
624 }
625 }
626
627 fn validate_operations(
630 initial_balance_changes: &[(BalanceChange, Currency)],
631 new_operations: &[Operation],
632 ) -> Result<(), anyhow::Error> {
633 let balances: HashMap<(SuiAddress, Currency), i128> = HashMap::new();
634 let mut initial_balances =
635 initial_balance_changes
636 .iter()
637 .fold(balances, |mut balances, (balance_change, ccy)| {
638 if let Owner::AddressOwner(owner) = balance_change.owner {
639 *balances.entry((owner, ccy.clone())).or_default() += balance_change.amount;
640 }
641 balances
642 });
643
644 let mut new_balances = HashMap::new();
645 for op in new_operations {
646 if let Some(Amount {
647 currency, value, ..
648 }) = &op.amount
649 {
650 if let Some(account) = &op.account {
651 let balance_change = new_balances
652 .remove(&(account.address, currency.clone()))
653 .unwrap_or(0)
654 + value;
655 new_balances.insert((account.address, currency.clone()), balance_change);
656 } else {
657 return Err(anyhow!("Missing account for a balance-change"));
658 }
659 }
660 }
661
662 for ((address, currency), amount_expected) in new_balances {
663 let new_amount = initial_balances.remove(&(address, currency)).unwrap_or(0);
664 if new_amount != amount_expected {
665 return Err(anyhow!(
666 "Expected {} balance-change for {} but got {}",
667 amount_expected,
668 address,
669 new_amount
670 ));
671 }
672 }
673 if !initial_balances.is_empty() {
674 return Err(anyhow!(
675 "Expected every item in initial_balances to be mapped"
676 ));
677 }
678 Ok(())
679 }
680
681 fn process_gascoin_transfer(
686 coin_change_operations: &mut impl Iterator<Item = Operation>,
687 data: SuiTransactionBlockData,
688 new_gas_owner: SuiAddress,
689 gas_used: i128,
690 initial_balance_changes: &[(BalanceChange, Currency)],
691 ) -> Result<Vec<Operation>, anyhow::Error> {
692 let tx = data.transaction();
693 let prev_gas_owner = data.gas_data().owner;
694 let mut operations = vec![];
695 if Self::is_gascoin_transfer(tx) && prev_gas_owner != new_gas_owner {
696 operations = coin_change_operations.collect();
697 Self::add_missing_gas_owner(&mut operations, prev_gas_owner);
698 Self::add_missing_gas_owner(&mut operations, new_gas_owner);
699 for operation in &mut operations {
700 match operation.type_ {
701 OperationType::Gas => {
702 operation.account = Some(prev_gas_owner.into())
705 }
706 OperationType::SuiBalanceChange => {
707 let account = operation
708 .account
709 .as_ref()
710 .ok_or_else(|| anyhow!("Missing account for a balance-change"))?;
711 let amount = operation
712 .amount
713 .as_mut()
714 .ok_or_else(|| anyhow!("Missing amount for a balance-change"))?;
715 if account.address == prev_gas_owner && amount.currency == *SUI {
717 amount.value -= gas_used;
718 } else if account.address == new_gas_owner && amount.currency == *SUI {
719 amount.value += gas_used;
720 }
721 }
722 _ => {
723 return Err(anyhow!(
724 "Discarding unsupported operation type {:?}",
725 operation.type_
726 ));
727 }
728 }
729 }
730 Self::validate_operations(initial_balance_changes, &operations)?;
731 }
732 Ok(operations)
733 }
734}
735
736impl Operations {
737 fn try_from_data(
738 data: SuiTransactionBlockData,
739 status: Option<OperationStatus>,
740 ) -> Result<Self, anyhow::Error> {
741 let sender = *data.sender();
742 Ok(Self::new(Self::from_transaction(
743 data.transaction().clone(),
744 sender,
745 status,
746 )?))
747 }
748}
749impl Operations {
750 pub async fn try_from_response(
751 response: SuiTransactionBlockResponse,
752 cache: &CoinMetadataCache,
753 ) -> Result<Self, Error> {
754 let tx = response
755 .transaction
756 .ok_or_else(|| anyhow!("Response input should not be empty"))?;
757 let sender = *tx.data.sender();
758 let effect = response
759 .effects
760 .ok_or_else(|| anyhow!("Response effects should not be empty"))?;
761 let gas_owner = effect.gas_object().owner.get_owner_address()?;
762 let gas_summary = effect.gas_cost_summary();
763 let gas_used = gas_summary.storage_rebate as i128
764 - gas_summary.storage_cost as i128
765 - gas_summary.computation_cost as i128;
766
767 let status = Some(effect.into_status().into());
768 let ops = Operations::try_from_data(tx.data.clone(), status)?;
769 let ops = ops.into_iter();
770
771 let mut accounted_balances =
774 ops.as_ref()
775 .iter()
776 .fold(HashMap::new(), |mut balances, op| {
777 if let (Some(acc), Some(amount), Some(OperationStatus::Success)) =
778 (&op.account, &op.amount, &op.status)
779 {
780 *balances
781 .entry((acc.address, amount.clone().currency))
782 .or_default() -= amount.value;
783 }
784 balances
785 });
786
787 let mut principal_amounts = 0;
788 let mut reward_amounts = 0;
789 if let Some(events) = response.events {
792 for event in events.data {
793 if is_unstake_event(&event.type_) {
794 let principal_amount = event
795 .parsed_json
796 .pointer("/principal_amount")
797 .and_then(|v| v.as_str())
798 .and_then(|v| i128::from_str(v).ok());
799 let reward_amount = event
800 .parsed_json
801 .pointer("/reward_amount")
802 .and_then(|v| v.as_str())
803 .and_then(|v| i128::from_str(v).ok());
804 if let (Some(principal_amount), Some(reward_amount)) =
805 (principal_amount, reward_amount)
806 {
807 principal_amounts += principal_amount;
808 reward_amounts += reward_amount;
809 }
810 }
811 }
812 }
813 let staking_balance = if principal_amounts != 0 {
814 *accounted_balances.entry((sender, SUI.clone())).or_default() -= principal_amounts;
815 *accounted_balances.entry((sender, SUI.clone())).or_default() -= reward_amounts;
816 vec![
817 Operation::stake_principle(status, sender, principal_amounts),
818 Operation::stake_reward(status, sender, reward_amounts),
819 ]
820 } else {
821 vec![]
822 };
823
824 let mut balance_changes = vec![];
825
826 for balance_change in &response
827 .balance_changes
828 .ok_or_else(|| anyhow!("Response balance changes should not be empty."))?
829 {
830 if let Ok(currency) = cache.get_currency(&balance_change.coin_type).await
831 && !currency.symbol.is_empty()
832 {
833 balance_changes.push((balance_change.clone(), currency));
834 }
835 }
836
837 let mut coin_change_operations = Self::process_balance_change(
839 gas_owner,
840 gas_used,
841 &balance_changes,
842 status,
843 accounted_balances.clone(),
844 );
845
846 let gascoin_transfer_operations = Self::process_gascoin_transfer(
849 &mut coin_change_operations,
850 tx.data,
851 gas_owner,
852 gas_used,
853 &balance_changes,
854 )?;
855
856 let ops: Operations = ops
857 .into_iter()
858 .chain(coin_change_operations)
859 .chain(gascoin_transfer_operations)
860 .chain(staking_balance)
861 .collect();
862
863 let mutually_cancelling_balances: HashMap<_, _> = ops
868 .clone()
869 .into_iter()
870 .fold(
871 HashMap::new(),
872 |mut balances: HashMap<(SuiAddress, Currency), i128>, op| {
873 if let (Some(acc), Some(amount), Some(OperationStatus::Success)) =
874 (&op.account, &op.amount, &op.status)
875 && op.type_ != OperationType::Gas
876 {
877 *balances
878 .entry((acc.address, amount.clone().currency))
879 .or_default() += amount.value;
880 }
881 balances
882 },
883 )
884 .into_iter()
885 .filter(|balance| {
886 let (_, amount) = balance;
887 *amount == 0
888 })
889 .collect();
890
891 let ops: Operations = ops
892 .into_iter()
893 .filter(|op| {
894 if let (Some(acc), Some(amount)) = (&op.account, &op.amount) {
895 return op.type_ == OperationType::Gas
896 || !mutually_cancelling_balances
897 .contains_key(&(acc.address, amount.clone().currency));
898 }
899 true
900 })
901 .collect();
902 Ok(ops)
903 }
904}
905
906fn is_unstake_event(tag: &StructTag) -> bool {
907 tag.address == SUI_SYSTEM_ADDRESS
908 && tag.module.as_ident_str() == ident_str!("validator")
909 && tag.name.as_ident_str() == ident_str!("UnstakingRequestEvent")
910}
911
912impl TryFrom<TransactionData> for Operations {
913 type Error = Error;
914 fn try_from(data: TransactionData) -> Result<Self, Self::Error> {
915 struct NoOpsModuleResolver;
916 impl ModuleResolver for NoOpsModuleResolver {
917 type Error = Error;
918 fn get_module(&self, _id: &ModuleId) -> Result<Option<Vec<u8>>, Self::Error> {
919 Ok(None)
920 }
921 }
922 Ok(Operations::try_from_data(
924 SuiTransactionBlockData::try_from_with_module_cache(data, &&mut NoOpsModuleResolver)?,
925 None,
926 )?)
927 }
928}
929
930#[derive(Deserialize, Serialize, Clone, Debug)]
931pub struct Operation {
932 operation_identifier: OperationIdentifier,
933 #[serde(rename = "type")]
934 pub type_: OperationType,
935 #[serde(default, skip_serializing_if = "Option::is_none")]
936 pub status: Option<OperationStatus>,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub account: Option<AccountIdentifier>,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub amount: Option<Amount>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub coin_change: Option<CoinChange>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
944 pub metadata: Option<OperationMetadata>,
945}
946
947impl PartialEq for Operation {
948 fn eq(&self, other: &Self) -> bool {
949 self.operation_identifier == other.operation_identifier
950 && self.type_ == other.type_
951 && self.account == other.account
952 && self.amount == other.amount
953 && self.coin_change == other.coin_change
954 && self.metadata == other.metadata
955 }
956}
957
958#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
959pub enum OperationMetadata {
960 GenericTransaction(SuiTransactionBlockKind),
961 Stake { validator: SuiAddress },
962 WithdrawStake { stake_ids: Vec<ObjectID> },
963}
964
965impl Operation {
966 fn generic_op(
967 status: Option<OperationStatus>,
968 sender: SuiAddress,
969 tx: SuiTransactionBlockKind,
970 ) -> Self {
971 Operation {
972 operation_identifier: Default::default(),
973 type_: (&tx).into(),
974 status,
975 account: Some(sender.into()),
976 amount: None,
977 coin_change: None,
978 metadata: Some(OperationMetadata::GenericTransaction(tx)),
979 }
980 }
981
982 pub fn genesis(index: u64, sender: SuiAddress, coin: GasCoin) -> Self {
983 Operation {
984 operation_identifier: index.into(),
985 type_: OperationType::Genesis,
986 status: Some(OperationStatus::Success),
987 account: Some(sender.into()),
988 amount: Some(Amount::new(coin.value().into(), None)),
989 coin_change: Some(CoinChange {
990 coin_identifier: CoinIdentifier {
991 identifier: CoinID {
992 id: *coin.id(),
993 version: SequenceNumber::new(),
994 },
995 },
996 coin_action: CoinAction::CoinCreated,
997 }),
998 metadata: None,
999 }
1000 }
1001
1002 fn pay_sui(status: Option<OperationStatus>, address: SuiAddress, amount: i128) -> Self {
1003 Operation {
1004 operation_identifier: Default::default(),
1005 type_: OperationType::PaySui,
1006 status,
1007 account: Some(address.into()),
1008 amount: Some(Amount::new(amount, None)),
1009 coin_change: None,
1010 metadata: None,
1011 }
1012 }
1013
1014 fn pay_coin(
1015 status: Option<OperationStatus>,
1016 address: SuiAddress,
1017 amount: i128,
1018 currency: Option<Currency>,
1019 ) -> Self {
1020 Operation {
1021 operation_identifier: Default::default(),
1022 type_: OperationType::PayCoin,
1023 status,
1024 account: Some(address.into()),
1025 amount: Some(Amount::new(amount, currency)),
1026 coin_change: None,
1027 metadata: None,
1028 }
1029 }
1030
1031 fn balance_change(
1032 status: Option<OperationStatus>,
1033 addr: SuiAddress,
1034 amount: i128,
1035 currency: Currency,
1036 ) -> Self {
1037 Self {
1038 operation_identifier: Default::default(),
1039 type_: OperationType::SuiBalanceChange,
1040 status,
1041 account: Some(addr.into()),
1042 amount: Some(Amount::new(amount, Some(currency))),
1043 coin_change: None,
1044 metadata: None,
1045 }
1046 }
1047 fn gas(addr: SuiAddress, amount: i128) -> Self {
1048 Self {
1049 operation_identifier: Default::default(),
1050 type_: OperationType::Gas,
1051 status: Some(OperationStatus::Success),
1052 account: Some(addr.into()),
1053 amount: Some(Amount::new(amount, None)),
1054 coin_change: None,
1055 metadata: None,
1056 }
1057 }
1058 fn stake_reward(status: Option<OperationStatus>, addr: SuiAddress, amount: i128) -> Self {
1059 Self {
1060 operation_identifier: Default::default(),
1061 type_: OperationType::StakeReward,
1062 status,
1063 account: Some(addr.into()),
1064 amount: Some(Amount::new(amount, None)),
1065 coin_change: None,
1066 metadata: None,
1067 }
1068 }
1069 fn stake_principle(status: Option<OperationStatus>, addr: SuiAddress, amount: i128) -> Self {
1070 Self {
1071 operation_identifier: Default::default(),
1072 type_: OperationType::StakePrinciple,
1073 status,
1074 account: Some(addr.into()),
1075 amount: Some(Amount::new(amount, None)),
1076 coin_change: None,
1077 metadata: None,
1078 }
1079 }
1080}