1use std::{collections::HashMap, ops::Not, str::FromStr, vec};
6
7use anyhow::anyhow;
8use iota_json_rpc_types::{
9 BalanceChange, IotaArgument, IotaCallArg, IotaCommand, IotaProgrammableMoveCall,
10 IotaProgrammableTransactionBlock,
11};
12use iota_sdk::rpc_types::{
13 IotaTransactionBlockData, IotaTransactionBlockDataAPI, IotaTransactionBlockEffectsAPI,
14 IotaTransactionBlockKind, IotaTransactionBlockResponse,
15};
16use iota_types::{
17 IOTA_SYSTEM_ADDRESS, IOTA_SYSTEM_PACKAGE_ID,
18 base_types::{IotaAddress, ObjectID, SequenceNumber},
19 digests::TransactionDigest,
20 gas_coin::{GAS, GasCoin},
21 governance::{ADD_STAKE_FUN_NAME, WITHDRAW_STAKE_FUN_NAME},
22 iota_system_state::IOTA_SYSTEM_MODULE_NAME,
23 object::Owner,
24 transaction::TransactionData,
25};
26use move_core_types::{
27 ident_str,
28 language_storage::{ModuleId, StructTag},
29 resolver::ModuleResolver,
30};
31use serde::{Deserialize, Serialize};
32
33use crate::{
34 Error,
35 types::{
36 AccountIdentifier, Amount, CoinAction, CoinChange, CoinID, CoinIdentifier,
37 InternalOperation, OperationIdentifier, OperationStatus, OperationType,
38 },
39};
40
41#[cfg(test)]
42#[path = "unit_tests/operations_tests.rs"]
43mod operations_tests;
44
45#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
46pub struct Operations(Vec<Operation>);
47
48impl FromIterator<Operation> for Operations {
49 fn from_iter<T: IntoIterator<Item = Operation>>(iter: T) -> Self {
50 Operations::new(iter.into_iter().collect())
51 }
52}
53
54impl FromIterator<Vec<Operation>> for Operations {
55 fn from_iter<T: IntoIterator<Item = Vec<Operation>>>(iter: T) -> Self {
56 iter.into_iter().flatten().collect()
57 }
58}
59
60impl IntoIterator for Operations {
61 type Item = Operation;
62 type IntoIter = vec::IntoIter<Operation>;
63 fn into_iter(self) -> Self::IntoIter {
64 self.0.into_iter()
65 }
66}
67
68impl Operations {
69 pub fn new(mut ops: Vec<Operation>) -> Self {
70 for (index, op) in ops.iter_mut().enumerate() {
71 op.operation_identifier = (index as u64).into()
72 }
73 Self(ops)
74 }
75
76 pub fn contains(&self, other: &Operations) -> bool {
77 for (i, other_op) in other.0.iter().enumerate() {
78 if let Some(op) = self.0.get(i) {
79 if op != other_op {
80 return false;
81 }
82 } else {
83 return false;
84 }
85 }
86 true
87 }
88
89 pub fn set_status(mut self, status: Option<OperationStatus>) -> Self {
90 for op in &mut self.0 {
91 op.status = status
92 }
93 self
94 }
95
96 pub fn type_(&self) -> Option<OperationType> {
97 self.0.first().map(|op| op.type_)
98 }
99
100 pub fn into_internal(self) -> Result<InternalOperation, Error> {
103 let type_ = self
104 .type_()
105 .ok_or_else(|| Error::MissingInput("Operation type".into()))?;
106 match type_ {
107 OperationType::PayIota => self.pay_iota_ops_to_internal(),
108 OperationType::Stake => self.stake_ops_to_internal(),
109 OperationType::WithdrawStake => self.withdraw_stake_ops_to_internal(),
110 op => Err(Error::UnsupportedOperation(op)),
111 }
112 }
113
114 fn pay_iota_ops_to_internal(self) -> Result<InternalOperation, Error> {
115 let mut recipients = vec![];
116 let mut amounts = vec![];
117 let mut sender = None;
118 for op in self {
119 if let (Some(amount), Some(account)) = (op.amount.clone(), op.account.clone()) {
120 if amount.value.is_negative() {
121 sender = Some(account.address)
122 } else {
123 recipients.push(account.address);
124 let amount = amount.value.abs();
125 if amount > u64::MAX as i128 {
126 return Err(Error::InvalidInput(
127 "Input amount exceed u64::MAX".to_string(),
128 ));
129 }
130 amounts.push(amount as u64)
131 }
132 }
133 }
134 let sender = sender.ok_or_else(|| Error::MissingInput("Sender address".to_string()))?;
135 Ok(InternalOperation::PayIota {
136 sender,
137 recipients,
138 amounts,
139 })
140 }
141
142 fn stake_ops_to_internal(self) -> Result<InternalOperation, Error> {
143 let mut ops = self
144 .0
145 .into_iter()
146 .filter(|op| op.type_ == OperationType::Stake)
147 .collect::<Vec<_>>();
148 if ops.len() != 1 {
149 return Err(Error::MalformedOperation(
150 "Delegation should only have one operation.".into(),
151 ));
152 }
153 let op = ops.pop().unwrap();
155 let sender = op
156 .account
157 .ok_or_else(|| Error::MissingInput("Sender address".to_string()))?
158 .address;
159 let metadata = op
160 .metadata
161 .ok_or_else(|| Error::MissingInput("Stake metadata".to_string()))?;
162
163 let amount = if let Some(amount) = op.amount {
165 if amount.value.is_positive() {
166 return Err(Error::MalformedOperation(
167 "Stake amount should be negative.".into(),
168 ));
169 }
170 Some(amount.value.unsigned_abs() as u64)
171 } else {
172 None
173 };
174
175 let OperationMetadata::Stake { validator } = metadata else {
176 return Err(Error::InvalidInput(
177 "Cannot find delegation info from metadata.".into(),
178 ));
179 };
180
181 Ok(InternalOperation::Stake {
182 sender,
183 validator,
184 amount,
185 })
186 }
187
188 fn withdraw_stake_ops_to_internal(self) -> Result<InternalOperation, Error> {
189 let mut ops = self
190 .0
191 .into_iter()
192 .filter(|op| op.type_ == OperationType::WithdrawStake)
193 .collect::<Vec<_>>();
194 if ops.len() != 1 {
195 return Err(Error::MalformedOperation(
196 "Delegation should only have one operation.".into(),
197 ));
198 }
199 let op = ops.pop().unwrap();
201 let sender = op
202 .account
203 .ok_or_else(|| Error::MissingInput("Sender address".to_string()))?
204 .address;
205
206 let stake_ids = if let Some(metadata) = op.metadata {
207 let OperationMetadata::WithdrawStake { stake_ids } = metadata else {
208 return Err(Error::InvalidInput(
209 "Cannot find withdraw stake info from metadata.".into(),
210 ));
211 };
212 stake_ids
213 } else {
214 vec![]
215 };
216
217 Ok(InternalOperation::WithdrawStake { sender, stake_ids })
218 }
219
220 fn from_transaction(
221 tx: IotaTransactionBlockKind,
222 sender: IotaAddress,
223 status: Option<OperationStatus>,
224 ) -> Result<Vec<Operation>, Error> {
225 Ok(match tx {
226 IotaTransactionBlockKind::ProgrammableTransaction(pt) => {
227 Self::parse_programmable_transaction(sender, status, pt)?
228 }
229 _ => vec![Operation::generic_op(status, sender, tx)],
230 })
231 }
232
233 pub fn from_transaction_data(
234 data: TransactionData,
235 digest: impl Into<Option<TransactionDigest>>,
236 ) -> Result<Self, Error> {
237 struct NoOpsModuleResolver;
238 impl ModuleResolver for NoOpsModuleResolver {
239 type Error = Error;
240 fn get_module(&self, _id: &ModuleId) -> Result<Option<Vec<u8>>, Self::Error> {
241 Ok(None)
242 }
243 }
244
245 let digest = digest.into().unwrap_or_default();
246
247 IotaTransactionBlockData::try_from(data, &&mut NoOpsModuleResolver, digest)?.try_into()
249 }
250
251 fn parse_programmable_transaction(
252 sender: IotaAddress,
253 status: Option<OperationStatus>,
254 pt: IotaProgrammableTransactionBlock,
255 ) -> Result<Vec<Operation>, Error> {
256 #[derive(Debug)]
257 enum KnownValue {
258 GasCoin(u64),
259 }
260 fn resolve_result(
261 known_results: &[Vec<KnownValue>],
262 i: u16,
263 j: u16,
264 ) -> Option<&KnownValue> {
265 known_results
266 .get(i as usize)
267 .and_then(|inner| inner.get(j as usize))
268 }
269 fn split_coins(
270 inputs: &[IotaCallArg],
271 known_results: &[Vec<KnownValue>],
272 coin: IotaArgument,
273 amounts: &[IotaArgument],
274 ) -> Option<Vec<KnownValue>> {
275 match coin {
276 IotaArgument::Result(i) => {
277 let KnownValue::GasCoin(_) = resolve_result(known_results, i, 0)?;
278 }
279 IotaArgument::NestedResult(i, j) => {
280 let KnownValue::GasCoin(_) = resolve_result(known_results, i, j)?;
281 }
282 IotaArgument::GasCoin => (),
283 IotaArgument::Input(_) => return None,
285 };
286 let amounts = amounts
287 .iter()
288 .map(|amount| {
289 let value: u64 = match *amount {
290 IotaArgument::Input(i) => {
291 u64::from_str(inputs[i as usize].pure()?.to_json_value().as_str()?)
292 .ok()?
293 }
294 IotaArgument::GasCoin
295 | IotaArgument::Result(_)
296 | IotaArgument::NestedResult(_, _) => return None,
297 };
298 Some(KnownValue::GasCoin(value))
299 })
300 .collect::<Option<_>>()?;
301 Some(amounts)
302 }
303 fn transfer_object(
304 aggregated_recipients: &mut HashMap<IotaAddress, u64>,
305 inputs: &[IotaCallArg],
306 known_results: &[Vec<KnownValue>],
307 objs: &[IotaArgument],
308 recipient: IotaArgument,
309 ) -> Option<Vec<KnownValue>> {
310 let addr = match recipient {
311 IotaArgument::Input(i) => inputs[i as usize].pure()?.to_iota_address().ok()?,
312 IotaArgument::GasCoin
313 | IotaArgument::Result(_)
314 | IotaArgument::NestedResult(_, _) => return None,
315 };
316 for obj in objs {
317 let value = match *obj {
318 IotaArgument::Result(i) => {
319 let KnownValue::GasCoin(value) = resolve_result(known_results, i, 0)?;
320 value
321 }
322 IotaArgument::NestedResult(i, j) => {
323 let KnownValue::GasCoin(value) = resolve_result(known_results, i, j)?;
324 value
325 }
326 IotaArgument::GasCoin | IotaArgument::Input(_) => return None,
327 };
328 let aggregate = aggregated_recipients.entry(addr).or_default();
329 *aggregate += value;
330 }
331 Some(vec![])
332 }
333 fn stake_call(
334 inputs: &[IotaCallArg],
335 known_results: &[Vec<KnownValue>],
336 call: &IotaProgrammableMoveCall,
337 ) -> Result<Option<(Option<u64>, IotaAddress)>, Error> {
338 let IotaProgrammableMoveCall { arguments, .. } = call;
339 let (amount, validator) = match &arguments[..] {
340 [_, coin, validator] => {
341 let amount = match coin {
342 IotaArgument::Result(i) => {
343 let KnownValue::GasCoin(value) = resolve_result(known_results, *i, 0)
344 .ok_or_else(|| {
345 anyhow!("Cannot resolve Gas coin value at Result({i})")
346 })?;
347 value
348 }
349 _ => return Ok(None),
350 };
351 let (some_amount, validator) = match validator {
352 IotaArgument::Input(i) => (
359 *i == 1,
360 inputs[*i as usize]
361 .pure()
362 .map(|v| v.to_iota_address())
363 .transpose(),
364 ),
365 _ => return Ok(None),
366 };
367 (some_amount.then_some(*amount), validator)
368 }
369 _ => Err(anyhow!(
370 "Error encountered when extracting arguments from move call, expecting 3 elements, got {}",
371 arguments.len()
372 ))?,
373 };
374 Ok(validator.map(|v| v.map(|v| (amount, v)))?)
375 }
376
377 fn unstake_call(
378 inputs: &[IotaCallArg],
379 call: &IotaProgrammableMoveCall,
380 ) -> Result<Option<ObjectID>, Error> {
381 let IotaProgrammableMoveCall { arguments, .. } = call;
382 let id = match &arguments[..] {
383 [_, stake_id] => {
384 match stake_id {
385 IotaArgument::Input(i) => {
386 let id = inputs[*i as usize]
387 .object()
388 .ok_or_else(|| anyhow!("Cannot find stake id from input args."))?;
389 let some_id = i % 2 == 1;
393 some_id.then_some(id)
394 }
395 _ => return Ok(None),
396 }
397 }
398 _ => Err(anyhow!(
399 "Error encountered when extracting arguments from move call, expecting 3 elements, got {}",
400 arguments.len()
401 ))?,
402 };
403 Ok(id.cloned())
404 }
405 let IotaProgrammableTransactionBlock { inputs, commands } = &pt;
406 let mut known_results: Vec<Vec<KnownValue>> = vec![];
407 let mut aggregated_recipients: HashMap<IotaAddress, u64> = HashMap::new();
408 let mut needs_generic = false;
409 let mut operations = vec![];
410 let mut stake_ids = vec![];
411 for command in commands {
412 let result = match command {
413 IotaCommand::SplitCoins(coin, amounts) => {
414 split_coins(inputs, &known_results, *coin, amounts)
415 }
416 IotaCommand::TransferObjects(objs, addr) => transfer_object(
417 &mut aggregated_recipients,
418 inputs,
419 &known_results,
420 objs,
421 *addr,
422 ),
423 IotaCommand::MoveCall(m) if Self::is_stake_call(m) => {
424 stake_call(inputs, &known_results, m)?.map(|(amount, validator)| {
425 let amount = amount.map(|amount| Amount::new(-(amount as i128)));
426 operations.push(Operation {
427 operation_identifier: Default::default(),
428 type_: OperationType::Stake,
429 status,
430 account: Some(sender.into()),
431 amount,
432 coin_change: None,
433 metadata: Some(OperationMetadata::Stake { validator }),
434 });
435 vec![]
436 })
437 }
438 IotaCommand::MoveCall(m) if Self::is_unstake_call(m) => {
439 let stake_id = unstake_call(inputs, m)?;
440 stake_ids.push(stake_id);
441 Some(vec![])
442 }
443 _ => None,
444 };
445 if let Some(result) = result {
446 known_results.push(result)
447 } else {
448 needs_generic = true;
449 break;
450 }
451 }
452
453 if !needs_generic && !aggregated_recipients.is_empty() {
454 let total_paid: u64 = aggregated_recipients.values().copied().sum();
455 operations.extend(
456 aggregated_recipients
457 .into_iter()
458 .map(|(recipient, amount)| {
459 Operation::pay_iota(status, recipient, amount.into())
460 }),
461 );
462 operations.push(Operation::pay_iota(status, sender, -(total_paid as i128)));
463 } else if !stake_ids.is_empty() {
464 let stake_ids = stake_ids.into_iter().flatten().collect::<Vec<_>>();
465 let metadata = stake_ids
466 .is_empty()
467 .not()
468 .then_some(OperationMetadata::WithdrawStake { stake_ids });
469 operations.push(Operation {
470 operation_identifier: Default::default(),
471 type_: OperationType::WithdrawStake,
472 status,
473 account: Some(sender.into()),
474 amount: None,
475 coin_change: None,
476 metadata,
477 });
478 } else if operations.is_empty() {
479 operations.push(Operation::generic_op(
480 status,
481 sender,
482 IotaTransactionBlockKind::ProgrammableTransaction(pt),
483 ))
484 }
485 Ok(operations)
486 }
487
488 fn is_stake_call(tx: &IotaProgrammableMoveCall) -> bool {
489 tx.package == IOTA_SYSTEM_PACKAGE_ID
490 && tx.module == IOTA_SYSTEM_MODULE_NAME.as_str()
491 && tx.function == ADD_STAKE_FUN_NAME.as_str()
492 }
493
494 fn is_unstake_call(tx: &IotaProgrammableMoveCall) -> bool {
495 tx.package == IOTA_SYSTEM_PACKAGE_ID
496 && tx.module == IOTA_SYSTEM_MODULE_NAME.as_str()
497 && tx.function == WITHDRAW_STAKE_FUN_NAME.as_str()
498 }
499
500 fn process_balance_change(
501 gas_owner: IotaAddress,
502 gas_used: i128,
503 balance_changes: &[BalanceChange],
504 status: Option<OperationStatus>,
505 balances: HashMap<IotaAddress, i128>,
506 ) -> impl Iterator<Item = Operation> {
507 let mut balances = balance_changes
508 .iter()
509 .fold(balances, |mut balances, balance_change| {
510 if let Owner::AddressOwner(owner) = balance_change.owner {
512 if balance_change.coin_type == GAS::type_tag() {
513 *balances.entry(owner).or_default() += balance_change.amount;
514 }
515 }
516 balances
517 });
518 *balances.entry(gas_owner).or_default() -= gas_used;
520
521 let balance_change = balances
522 .into_iter()
523 .filter(|(_, amount)| *amount != 0)
524 .map(move |(addr, amount)| Operation::balance_change(status, addr, amount));
525
526 let gas = if gas_used != 0 {
527 vec![Operation::gas(gas_owner, gas_used)]
528 } else {
529 vec![]
531 };
532 balance_change.chain(gas)
533 }
534}
535
536impl TryFrom<IotaTransactionBlockData> for Operations {
537 type Error = Error;
538 fn try_from(data: IotaTransactionBlockData) -> Result<Self, Self::Error> {
539 let sender = *data.sender();
540 Ok(Self::new(Self::from_transaction(
541 data.transaction().clone(),
542 sender,
543 None,
544 )?))
545 }
546}
547
548impl TryFrom<IotaTransactionBlockResponse> for Operations {
549 type Error = Error;
550 fn try_from(response: IotaTransactionBlockResponse) -> Result<Self, Self::Error> {
551 let tx = response
552 .transaction
553 .ok_or_else(|| anyhow!("Response input should not be empty"))?;
554 let sender = *tx.data.sender();
555 let effect = response
556 .effects
557 .ok_or_else(|| anyhow!("Response effects should not be empty"))?;
558 let gas_owner = effect.gas_object().owner.get_owner_address()?;
559 let gas_summary = effect.gas_cost_summary();
560 let gas_used = gas_summary.storage_rebate as i128
561 - gas_summary.storage_cost as i128
562 - gas_summary.computation_cost as i128;
563
564 let status = Some(effect.into_status().into());
565 let ops: Operations = tx.data.try_into()?;
566 let ops = ops.set_status(status).into_iter();
567
568 let mut accounted_balances =
571 ops.as_ref()
572 .iter()
573 .fold(HashMap::new(), |mut balances, op| {
574 if let (Some(acc), Some(amount), Some(OperationStatus::Success)) =
575 (&op.account, &op.amount, &op.status)
576 {
577 *balances.entry(acc.address).or_default() -= amount.value;
578 }
579 balances
580 });
581
582 let mut principal_amounts = 0;
583 let mut reward_amounts = 0;
584 if let Some(events) = response.events {
587 for event in events.data {
588 if is_unstake_event(&event.type_) {
589 let principal_amount = event
590 .parsed_json
591 .pointer("/principal_amount")
592 .and_then(|v| v.as_str())
593 .and_then(|v| i128::from_str(v).ok());
594 let reward_amount = event
595 .parsed_json
596 .pointer("/reward_amount")
597 .and_then(|v| v.as_str())
598 .and_then(|v| i128::from_str(v).ok());
599 if let (Some(principal_amount), Some(reward_amount)) =
600 (principal_amount, reward_amount)
601 {
602 principal_amounts += principal_amount;
603 reward_amounts += reward_amount;
604 }
605 }
606 }
607 }
608 let staking_balance = if principal_amounts != 0 {
609 *accounted_balances.entry(sender).or_default() -= principal_amounts;
610 *accounted_balances.entry(sender).or_default() -= reward_amounts;
611 vec![
612 Operation::stake_principle(status, sender, principal_amounts),
613 Operation::stake_reward(status, sender, reward_amounts),
614 ]
615 } else {
616 vec![]
617 };
618
619 let coin_change_operations = Self::process_balance_change(
621 gas_owner,
622 gas_used,
623 &response
624 .balance_changes
625 .ok_or_else(|| anyhow!("Response balance changes should not be empty."))?,
626 status,
627 accounted_balances,
628 );
629
630 Ok(ops
631 .into_iter()
632 .chain(coin_change_operations)
633 .chain(staking_balance)
634 .collect())
635 }
636}
637
638fn is_unstake_event(tag: &StructTag) -> bool {
639 tag.address == IOTA_SYSTEM_ADDRESS
640 && tag.module.as_ident_str() == ident_str!("validator")
641 && tag.name.as_ident_str() == ident_str!("UnstakingRequestEvent")
642}
643
644#[derive(Deserialize, Serialize, Clone, Debug)]
645pub struct Operation {
646 operation_identifier: OperationIdentifier,
647 #[serde(rename = "type")]
648 pub type_: OperationType,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub status: Option<OperationStatus>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub account: Option<AccountIdentifier>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub amount: Option<Amount>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub coin_change: Option<CoinChange>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub metadata: Option<OperationMetadata>,
659}
660
661impl PartialEq for Operation {
662 fn eq(&self, other: &Self) -> bool {
663 self.operation_identifier == other.operation_identifier
664 && self.type_ == other.type_
665 && self.account == other.account
666 && self.amount == other.amount
667 && self.coin_change == other.coin_change
668 && self.metadata == other.metadata
669 }
670}
671
672#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
673pub enum OperationMetadata {
674 GenericTransaction(IotaTransactionBlockKind),
675 Stake { validator: IotaAddress },
676 WithdrawStake { stake_ids: Vec<ObjectID> },
677}
678
679impl Operation {
680 fn generic_op(
681 status: Option<OperationStatus>,
682 sender: IotaAddress,
683 tx: IotaTransactionBlockKind,
684 ) -> Self {
685 Operation {
686 operation_identifier: Default::default(),
687 type_: (&tx).into(),
688 status,
689 account: Some(sender.into()),
690 amount: None,
691 coin_change: None,
692 metadata: Some(OperationMetadata::GenericTransaction(tx)),
693 }
694 }
695
696 pub fn genesis(index: u64, sender: IotaAddress, coin: GasCoin) -> Self {
697 Operation {
698 operation_identifier: index.into(),
699 type_: OperationType::Genesis,
700 status: Some(OperationStatus::Success),
701 account: Some(sender.into()),
702 amount: Some(Amount::new(coin.value().into())),
703 coin_change: Some(CoinChange {
704 coin_identifier: CoinIdentifier {
705 identifier: CoinID {
706 id: *coin.id(),
707 version: SequenceNumber::new(),
708 },
709 },
710 coin_action: CoinAction::CoinCreated,
711 }),
712 metadata: None,
713 }
714 }
715
716 fn pay_iota(status: Option<OperationStatus>, address: IotaAddress, amount: i128) -> Self {
717 Operation {
718 operation_identifier: Default::default(),
719 type_: OperationType::PayIota,
720 status,
721 account: Some(address.into()),
722 amount: Some(Amount::new(amount)),
723 coin_change: None,
724 metadata: None,
725 }
726 }
727
728 fn balance_change(status: Option<OperationStatus>, addr: IotaAddress, amount: i128) -> Self {
729 Self {
730 operation_identifier: Default::default(),
731 type_: OperationType::IotaBalanceChange,
732 status,
733 account: Some(addr.into()),
734 amount: Some(Amount::new(amount)),
735 coin_change: None,
736 metadata: None,
737 }
738 }
739 fn gas(addr: IotaAddress, amount: i128) -> Self {
740 Self {
741 operation_identifier: Default::default(),
742 type_: OperationType::Gas,
743 status: Some(OperationStatus::Success),
744 account: Some(addr.into()),
745 amount: Some(Amount::new(amount)),
746 coin_change: None,
747 metadata: None,
748 }
749 }
750 fn stake_reward(status: Option<OperationStatus>, addr: IotaAddress, amount: i128) -> Self {
751 Self {
752 operation_identifier: Default::default(),
753 type_: OperationType::StakeReward,
754 status,
755 account: Some(addr.into()),
756 amount: Some(Amount::new(amount)),
757 coin_change: None,
758 metadata: None,
759 }
760 }
761 fn stake_principle(status: Option<OperationStatus>, addr: IotaAddress, amount: i128) -> Self {
762 Self {
763 operation_identifier: Default::default(),
764 type_: OperationType::StakePrinciple,
765 status,
766 account: Some(addr.into()),
767 amount: Some(Amount::new(amount)),
768 coin_change: None,
769 metadata: None,
770 }
771 }
772}