iota_rosetta/
operations.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    /// Parse operation input from rosetta operation to intermediate internal
101    /// operation;
102    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        // Checked above, safe to unwrap.
154        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        // Total issued IOTA is less than u64, safe to cast.
164        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        // Checked above, safe to unwrap.
200        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        // Rosetta don't need the call args to be parsed into readable format
248        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                // Might not be an IOTA coin
284                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                        // [WORKAROUND] - this is a hack to work out if the staking ops is for a
353                        // selected amount or None amount (whole wallet). We
354                        // use the position of the validator arg as a indicator of if the rosetta
355                        // stake transaction is staking the whole wallet or
356                        // not, if staking whole wallet, we have to omit the
357                        // amount value in the final operation output.
358                        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                            // [WORKAROUND] - this is a hack to work out if the withdraw stake ops
390                            // is for a selected stake or None (all stakes).
391                            // this hack is similar to the one in stake_call.
392                            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                // Rosetta only care about address owner
511                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        // separate gas from balances
519        *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            // Gas can be 0 for system tx
530            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        // We will need to subtract the operation amounts from the actual balance
569        // change amount extracted from event to prevent double counting.
570        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        // Extract balance change from unstake events
585
586        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        // Extract coin change operations from balance changes
620        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}