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}