Skip to main content

sui_core/accumulators/
funds_read.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5
6use move_core_types::language_storage::TypeTag;
7use sui_types::{
8    accumulator_root::AccumulatorObjId,
9    balance::Balance,
10    base_types::{SequenceNumber, SuiAddress},
11    error::{SuiErrorKind, SuiResult, UserInputError},
12};
13
14pub trait AccountFundsRead: Send + Sync {
15    /// Gets the latest amount in an account. If the account does not exist, returns 0.
16    /// This does an unsequenced read and does not guarantee consistency with the root accumulator
17    /// version.
18    fn get_latest_account_amount(&self, account_id: &AccumulatorObjId) -> u128;
19
20    /// Gets the account amount at a version consistent with a stable accumulator root version.
21    /// If the account object has advanced ahead of that root version, this returns the amount
22    /// at or before the root version instead of the latest account object amount. If the account
23    /// does not exist at that root version, returns 0 for the amount.
24    /// It guarantees no data race between the read of the account object and the root accumulator version.
25    fn get_consistent_latest_account_amount_and_version(
26        &self,
27        account_id: &AccumulatorObjId,
28    ) -> (u128, SequenceNumber);
29
30    /// Read the amount at a precise version. Care must be taken to only call this function if we
31    /// can guarantee that objects behind this version have not yet been pruned.
32    fn get_account_amount_at_version(
33        &self,
34        account_id: &AccumulatorObjId,
35        version: SequenceNumber,
36    ) -> u128;
37
38    /// Checks if given amounts are available in the latest versions of the referenced acccumulator
39    /// objects. This does un-sequenced reads and can only be used on the signing/voting path
40    /// where deterministic results are not required.
41    fn check_amounts_available(
42        &self,
43        requested_amounts: &BTreeMap<AccumulatorObjId, (u64, TypeTag, SuiAddress)>,
44    ) -> SuiResult {
45        for (object_id, (requested_amount, type_tag, owner)) in requested_amounts {
46            let actual_amount = self.get_latest_account_amount(object_id);
47
48            if actual_amount < *requested_amount as u128 {
49                let coin_type = Balance::maybe_get_balance_type_param(type_tag)
50                    .unwrap_or_else(|| type_tag.clone());
51                return Err(SuiErrorKind::UserInputError {
52                    error: UserInputError::InvalidWithdrawReservation {
53                        error: format!(
54                            "Insufficient address balance of coin type {coin_type} \
55                             for address {owner}: the transaction requires \
56                             {requested_amount} but only {actual_amount} is available. \
57                             Note that the address balance does not include funds held \
58                             in Coin objects owned by the address; to spend those funds, \
59                             use the Coin objects directly as transaction inputs.",
60                        ),
61                    },
62                }
63                .into());
64            }
65        }
66
67        Ok(())
68    }
69
70    /// For gasless transactions, checks that withdrawing the requested amounts does not leave
71    /// a balance below the minimum in the sender's account. For each withdrawal, the remaining balance
72    /// (actual - requested) must be either 0 or >= the minimum transfer amount for that
73    /// token type.
74    fn check_remaining_amounts_after_withdrawal(
75        &self,
76        requested_amounts: &BTreeMap<AccumulatorObjId, (u64, TypeTag, SuiAddress)>,
77        min_amounts: &BTreeMap<TypeTag, u64>,
78    ) -> SuiResult {
79        for (object_id, (requested_amount, type_tag, owner)) in requested_amounts {
80            let actual_amount = self.get_latest_account_amount(object_id);
81            let remaining = actual_amount.saturating_sub(*requested_amount as u128);
82            if remaining == 0 {
83                continue;
84            }
85            let coin_type =
86                Balance::maybe_get_balance_type_param(type_tag).unwrap_or_else(|| type_tag.clone());
87            if let Some(&min_amount) = min_amounts.get(&coin_type)
88                && min_amount > 0
89                && remaining < min_amount as u128
90            {
91                return Err(SuiErrorKind::UserInputError {
92                    error: UserInputError::InvalidWithdrawReservation {
93                        error: format!(
94                            "Invalid gasless withdrawal of coin type {coin_type} \
95                             from address {owner}. \
96                             Gasless transactions must either use the entire address \
97                             balance, or leave at least {min_amount}. \
98                             Remaining amount would be {remaining}",
99                        ),
100                    },
101                }
102                .into());
103            }
104        }
105
106        Ok(())
107    }
108}