Skip to main content

thruput_sdk/storage/
balance.rs

1use super::keypath::{Keypath, LeafKey};
2use crate::exn::LibraryExn;
3use crate::re_throw::OrReThrow;
4use crate::sys_exn::nnint_internal as nnint;
5
6/// A non-negative, zero-defaulting balance backed by Thruput's mergeable
7/// integer-storage cell.
8///
9/// Values are in `0..=i128::MAX`. Operations reject a negative amount with
10/// [`LibraryExn::NonNegativeIntUnderflow`].
11///
12/// Each direction comes in a checked and a lazy form:
13///
14/// - [`increase`](Self::increase) and [`decrease`](Self::decrease) load the
15///   committed balance and fail immediately (during contract execution) if the
16///   result would overflow or go negative. Loading establishes a strict
17///   constraint against the state snapshot, and immediate failure makes the
18///   cause easier to locate.
19/// - [`lazy_increase`](Self::lazy_increase) and
20///   [`lazy_decrease`](Self::lazy_decrease) record a delta without loading the
21///   committed balance. Deltas from concurrent transactions can merge; the
22///   runtime checks the committed result when the transaction commits.
23///
24/// Unlike [`NNInt`](super::NNInt), `Balance` exposes no absolute `set` or
25/// signed-adjustment operation. A missing storage cell reads as zero.
26///
27/// `Balance` uses the same storage representation as `NNInt`, so an existing
28/// non-negative `NNInt` can be read as a balance. Setting that cell negative
29/// through the lower-level API violates the `Balance` invariant. In
30/// particular, the lazy operations do not load the committed value and
31/// therefore cannot detect invalid negative state introduced through another
32/// handle type.
33#[derive(Debug)]
34pub struct Balance {
35    key: LeafKey,
36}
37
38impl From<Keypath> for Balance {
39    fn from(keypath: Keypath) -> Self {
40        Self {
41            key: LeafKey::from(keypath),
42        }
43    }
44}
45
46impl Balance {
47    pub const MAX: i128 = i128::MAX;
48
49    /// Get the balance, treating a never-written location as zero.
50    pub fn get(&self) -> i128 {
51        self.try_get().or_re_throw()
52    }
53
54    /// Get the balance, reporting storage failures instead of throwing an
55    /// exception. A never-written location is `Ok(0)`. A negative value
56    /// written through the lower-level NNInt API is reported as an underflow.
57    pub fn try_get(&self) -> Result<i128, LibraryExn> {
58        match nnint::try_get_values_force(self.key.get()) {
59            Ok((set, add)) => {
60                let value = set
61                    .checked_add(add)
62                    .ok_or(LibraryExn::NonNegativeIntOverflow)?;
63                if value < 0 {
64                    return Err(LibraryExn::NonNegativeIntUnderflow);
65                }
66                Ok(value)
67            }
68            Err(e) if e.is_storage_missing() => Ok(0),
69            Err(e) => Err(e),
70        }
71    }
72
73    /// Increase by a non-negative amount, throwing immediately if the resulting
74    /// balance would overflow.
75    ///
76    /// This loads the current balance and therefore establishes a strict
77    /// constraint against the state snapshot. Use
78    /// [`lazy_increase`](Self::lazy_increase) when the increase should instead
79    /// merge with concurrent transactions.
80    ///
81    /// *Throws:* any error that would be returned by
82    /// [`try_increase`](Self::try_increase).
83    pub fn increase(&mut self, amount: i128) {
84        self.try_increase(amount).or_re_throw()
85    }
86
87    /// Like [`increase`](Self::increase), but report overflow or storage-type
88    /// failure as `Err` instead of aborting (the `try_` law; see the
89    /// [`exn`](mod@crate::exn) module docs). On `Err`, the stored value is
90    /// unmodified.
91    pub fn try_increase(&mut self, amount: i128) -> Result<(), LibraryExn> {
92        if amount == 0 {
93            return Ok(());
94        }
95        if amount < 0 {
96            return Err(LibraryExn::NonNegativeIntUnderflow);
97        }
98        let key = self.key.get();
99        let (set, add) = match nnint::try_get_values_force(key) {
100            Ok(values) => values,
101            Err(e) if e.is_storage_missing() => (0, 0),
102            Err(e) => return Err(e),
103        };
104        let add = add
105            .checked_add(amount)
106            .ok_or(LibraryExn::NonNegativeIntOverflow)?;
107        let total = set
108            .checked_add(add)
109            .ok_or(LibraryExn::NonNegativeIntOverflow)?;
110        if total < 0 {
111            return Err(LibraryExn::NonNegativeIntUnderflow);
112        }
113        nnint::try_set_add(key, Some(set), add, self.key.is_exclusive())
114    }
115
116    /// Decrease by a non-negative amount, throwing immediately if the balance
117    /// is insufficient.
118    ///
119    /// This loads the current balance and therefore establishes a strict
120    /// constraint against the state snapshot. Use
121    /// [`lazy_decrease`](Self::lazy_decrease) when the decrease should instead
122    /// merge with concurrent transactions.
123    ///
124    /// *Throws:* any error that would be returned by
125    /// [`try_decrease`](Self::try_decrease).
126    pub fn decrease(&mut self, amount: i128) {
127        self.try_decrease(amount).or_re_throw()
128    }
129
130    /// Like [`decrease`](Self::decrease), but report underflow or storage
131    /// failure as `Err` instead of aborting (the `try_` law; see the
132    /// [`exn`](mod@crate::exn) module docs).
133    ///
134    /// On `Err`, the stored value is unmodified.
135    pub fn try_decrease(&mut self, amount: i128) -> Result<(), LibraryExn> {
136        if amount == 0 {
137            return Ok(());
138        }
139        if amount < 0 {
140            return Err(LibraryExn::NonNegativeIntUnderflow);
141        }
142        let key = self.key.get();
143        let (set, add) = match nnint::try_get_values_force(key) {
144            Ok(values) => values,
145            Err(e) if e.is_storage_missing() => (0, 0),
146            Err(e) => return Err(e),
147        };
148        let add = add
149            .checked_sub(amount)
150            .ok_or(LibraryExn::NonNegativeIntUnderflow)?;
151        let total = set
152            .checked_add(add)
153            .ok_or(LibraryExn::NonNegativeIntOverflow)?;
154        if total < 0 {
155            return Err(LibraryExn::NonNegativeIntUnderflow);
156        }
157        nnint::try_set_add(key, Some(set), add, self.key.is_exclusive())
158    }
159
160    /// Increase by a non-negative amount without loading the current balance.
161    ///
162    /// This operation is deliberately mergeable: overflow against the
163    /// committed balance is checked when the transaction commits, not here.
164    /// Use [`increase`](Self::increase) when the contract needs an immediate
165    /// overflow result.
166    ///
167    /// *Throws:* any error that would be returned by
168    /// [`try_lazy_increase`](Self::try_lazy_increase).
169    pub fn lazy_increase(&mut self, amount: i128) {
170        self.try_lazy_increase(amount).or_re_throw()
171    }
172
173    /// Like [`lazy_increase`](Self::lazy_increase), but report immediate
174    /// conversion or accumulated-delta overflow as `Err` instead of aborting
175    /// (the `try_` law; see the [`exn`](mod@crate::exn) module docs).
176    ///
177    /// This does not turn the increase into checked arithmetic. Overflow
178    /// against the committed balance remains a commit-time check because a
179    /// lazy increase deliberately does not load that balance.
180    pub fn try_lazy_increase(
181        &mut self,
182        amount: i128,
183    ) -> Result<(), LibraryExn> {
184        if amount == 0 {
185            return Ok(());
186        }
187        if amount < 0 {
188            return Err(LibraryExn::NonNegativeIntUnderflow);
189        }
190        let key = self.key.get();
191        let (set, add) = nnint::try_get_values(key)?;
192        let add = add
193            .checked_add(amount)
194            .ok_or(LibraryExn::NonNegativeIntOverflow)?;
195        nnint::try_set_add(key, set, add, self.key.is_exclusive())
196    }
197
198    /// Decrease by a non-negative amount without loading the current balance.
199    ///
200    /// This operation is deliberately mergeable: sufficiency against the
201    /// committed balance is checked when the transaction commits, not here.
202    /// Use [`decrease`](Self::decrease) when the contract needs an immediate
203    /// insufficient-balance result.
204    ///
205    /// *Throws:* any error that would be returned by
206    /// [`try_lazy_decrease`](Self::try_lazy_decrease).
207    pub fn lazy_decrease(&mut self, amount: i128) {
208        self.try_lazy_decrease(amount).or_re_throw()
209    }
210
211    /// Like [`lazy_decrease`](Self::lazy_decrease), but report immediate
212    /// conversion or accumulated-delta underflow as `Err` instead of aborting
213    /// (the `try_` law; see the [`exn`](mod@crate::exn) module docs).
214    ///
215    /// This does not turn the decrease into checked arithmetic. Sufficiency
216    /// against the committed balance remains a commit-time check because a
217    /// lazy decrease deliberately does not load that balance. A balance
218    /// already written this transaction is the exception: that value is
219    /// pending in the storage layer rather than in the database, so it costs
220    /// no load and is checked here.
221    pub fn try_lazy_decrease(
222        &mut self,
223        amount: i128,
224    ) -> Result<(), LibraryExn> {
225        if amount == 0 {
226            return Ok(());
227        }
228        if amount < 0 {
229            return Err(LibraryExn::NonNegativeIntUnderflow);
230        }
231        let key = self.key.get();
232        let (set, add) = nnint::try_get_values(key)?;
233        let add = add
234            .checked_sub(amount)
235            .ok_or(LibraryExn::NonNegativeIntUnderflow)?;
236        if let Some(set) = set {
237            let total = set
238                .checked_add(add)
239                .ok_or(LibraryExn::NonNegativeIntOverflow)?;
240            if total < 0 {
241                return Err(LibraryExn::NonNegativeIntUnderflow);
242            }
243        }
244        nnint::try_set_add(key, set, add, self.key.is_exclusive())
245    }
246}
247
248impl super::sealed::Sealed for Balance {}
249impl super::node::StorageType for Balance {}
250
251impl super::flags::Flaggable for Balance {
252    fn storage_key(&self) -> &crate::types::StorageKey {
253        self.key.get()
254    }
255}
256
257impl super::deletable::Deletable for Balance {
258    fn delete_internal(&mut self, exclusive: bool) {
259        let exclusive = exclusive || self.key.is_exclusive();
260        crate::sys_exn::delete::delete(self.key.get(), exclusive);
261    }
262}