oasis_runtime_sdk/modules/accounts/
fee.rs1use std::collections::BTreeMap;
3
4use crate::types::{address::Address, token};
5
6#[derive(Clone, Default, Debug)]
11pub struct FeeManager {
12 tx_fee: Option<TransactionFee>,
14 block_fees: BTreeMap<token::Denomination, u128>,
16}
17
18#[derive(Clone, Default, Debug)]
20pub struct TransactionFee {
21 payer: Address,
23 denomination: token::Denomination,
25 charged: u128,
27 refunded: u128,
29}
30
31impl TransactionFee {
32 pub fn denomination(&self) -> token::Denomination {
34 self.denomination.clone()
35 }
36
37 pub fn amount(&self) -> u128 {
39 self.charged.saturating_sub(self.refunded)
40 }
41
42 pub fn payer(&self) -> Address {
44 self.payer
45 }
46}
47
48impl FeeManager {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn tx_fee(&self) -> Option<&TransactionFee> {
56 self.tx_fee.as_ref()
57 }
58
59 pub fn record_fee(&mut self, payer: Address, amount: &token::BaseUnits) {
64 let tx_fee = self.tx_fee.get_or_insert_with(|| TransactionFee {
65 payer,
66 denomination: amount.denomination().clone(),
67 ..Default::default()
68 });
69
70 assert!(payer == tx_fee.payer, "transaction fee payer cannot change");
71 assert!(
72 amount.denomination() == &tx_fee.denomination,
73 "transaction fee denomination cannot change"
74 );
75
76 tx_fee.charged = tx_fee
77 .charged
78 .checked_add(amount.amount())
79 .expect("should never overflow");
80 }
81
82 pub fn record_refund(&mut self, amount: u128) {
84 if amount == 0 || self.tx_fee.is_none() {
85 return;
86 }
87
88 let tx_fee = self.tx_fee.as_mut().unwrap();
89 tx_fee.refunded = std::cmp::min(tx_fee.refunded.saturating_add(amount), tx_fee.charged);
90 }
91
92 #[must_use = "fee updates should be applied after calling commit"]
98 pub fn commit_tx(&mut self) -> FeeUpdates {
99 let tx_fee = self.tx_fee.take().unwrap_or_default();
100 if tx_fee.amount() > 0 {
101 let block_fees = self
102 .block_fees
103 .entry(tx_fee.denomination.clone())
104 .or_default();
105
106 *block_fees = block_fees
108 .checked_add(tx_fee.amount())
109 .expect("should never overflow");
110 }
111
112 FeeUpdates {
113 payer: tx_fee.payer,
114 refund: token::BaseUnits::new(tx_fee.refunded, tx_fee.denomination),
115 }
116 }
117
118 #[must_use = "accumulated fees should be applied after calling commit"]
123 pub fn commit_block(self) -> BTreeMap<token::Denomination, u128> {
124 self.block_fees
125 }
126}
127
128pub struct FeeUpdates {
133 pub payer: Address,
135 pub refund: token::BaseUnits,
137}
138
139#[cfg(test)]
140mod test {
141 use super::*;
142
143 use crate::{
144 testing::keys,
145 types::token::{self, Denomination},
146 };
147
148 #[test]
149 fn test_basic_refund() {
150 let mut mgr = FeeManager::new();
151
152 assert!(mgr.tx_fee().is_none());
153
154 let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
156 mgr.record_fee(keys::alice::address(), &fee);
157
158 let tx_fee = mgr.tx_fee().expect("tx_fee should be set");
159 assert_eq!(tx_fee.payer(), keys::alice::address());
160 assert_eq!(&tx_fee.denomination(), fee.denomination());
161 assert_eq!(tx_fee.amount(), fee.amount());
162
163 mgr.record_refund(400_000);
164
165 let tx_fee = mgr.tx_fee().expect("tx_fee should be set");
166 assert_eq!(tx_fee.payer(), keys::alice::address());
167 assert_eq!(&tx_fee.denomination(), fee.denomination());
168 assert_eq!(tx_fee.amount(), 600_000, "should take refund into account");
169
170 let fee_updates = mgr.commit_tx();
171 assert_eq!(fee_updates.payer, keys::alice::address());
172 assert_eq!(
173 fee_updates.refund,
174 token::BaseUnits::new(400_000, Denomination::NATIVE)
175 );
176 assert!(mgr.tx_fee().is_none());
177
178 mgr.record_fee(
180 keys::bob::address(),
181 &token::BaseUnits::new(50_000, Denomination::NATIVE),
182 );
183 let fee_updates = mgr.commit_tx();
184 assert_eq!(fee_updates.payer, keys::bob::address());
185 assert_eq!(
186 fee_updates.refund,
187 token::BaseUnits::new(0, Denomination::NATIVE)
188 );
189
190 mgr.record_fee(
191 keys::dave::address(),
192 &token::BaseUnits::new(25_000, "TEST".parse().unwrap()),
193 );
194 mgr.record_fee(
195 keys::dave::address(),
196 &token::BaseUnits::new(5_000, "TEST".parse().unwrap()),
197 );
198 let fee_updates = mgr.commit_tx();
199 assert_eq!(fee_updates.payer, keys::dave::address());
200 assert_eq!(
201 fee_updates.refund,
202 token::BaseUnits::new(0, "TEST".parse().unwrap())
203 );
204
205 let block_fees = mgr.commit_block();
206 assert_eq!(block_fees.len(), 2);
207 assert_eq!(block_fees[&Denomination::NATIVE], 650_000);
208 assert_eq!(block_fees[&"TEST".parse().unwrap()], 30_000);
209 }
210
211 #[test]
212 fn test_refund_without_charge() {
213 let mut mgr = FeeManager::new();
214
215 mgr.record_refund(1_000);
216 assert!(
217 mgr.tx_fee().is_none(),
218 "refund should not be recorded if no charge"
219 );
220
221 let fee_updates = mgr.commit_tx();
222 assert_eq!(fee_updates.payer, Default::default());
223 assert_eq!(
224 fee_updates.refund,
225 token::BaseUnits::new(0, Default::default())
226 );
227
228 let block_fees = mgr.commit_block();
229 assert!(block_fees.is_empty(), "there should be no recorded fees");
230 }
231
232 #[test]
233 #[should_panic(expected = "transaction fee payer cannot change")]
234 fn test_fail_payer_change() {
235 let mut mgr = FeeManager::new();
236
237 let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
238 mgr.record_fee(keys::alice::address(), &fee);
239 mgr.record_fee(keys::bob::address(), &fee); }
241
242 #[test]
243 #[should_panic(expected = "transaction fee denomination cannot change")]
244 fn test_fail_denomination_change() {
245 let mut mgr = FeeManager::new();
246
247 let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
248 mgr.record_fee(keys::alice::address(), &fee);
249
250 let fee = token::BaseUnits::new(1_000_000, "TEST".parse().unwrap());
251 mgr.record_fee(keys::alice::address(), &fee); }
253}