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::native(1_000_000);
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!(fee_updates.refund, token::BaseUnits::native(400_000));
173 assert!(mgr.tx_fee().is_none());
174
175 mgr.record_fee(keys::bob::address(), &token::BaseUnits::native(50_000));
177 let fee_updates = mgr.commit_tx();
178 assert_eq!(fee_updates.payer, keys::bob::address());
179 assert_eq!(fee_updates.refund, token::BaseUnits::default());
180
181 mgr.record_fee(
182 keys::dave::address(),
183 &token::BaseUnits::new(25_000, "TEST".parse().unwrap()),
184 );
185 mgr.record_fee(
186 keys::dave::address(),
187 &token::BaseUnits::new(5_000, "TEST".parse().unwrap()),
188 );
189 let fee_updates = mgr.commit_tx();
190 assert_eq!(fee_updates.payer, keys::dave::address());
191 assert_eq!(
192 fee_updates.refund,
193 token::BaseUnits::new(0, "TEST".parse().unwrap())
194 );
195
196 let block_fees = mgr.commit_block();
197 assert_eq!(block_fees.len(), 2);
198 assert_eq!(block_fees[&Denomination::NATIVE], 650_000);
199 assert_eq!(block_fees[&"TEST".parse().unwrap()], 30_000);
200 }
201
202 #[test]
203 fn test_refund_without_charge() {
204 let mut mgr = FeeManager::new();
205
206 mgr.record_refund(1_000);
207 assert!(
208 mgr.tx_fee().is_none(),
209 "refund should not be recorded if no charge"
210 );
211
212 let fee_updates = mgr.commit_tx();
213 assert_eq!(fee_updates.payer, Default::default());
214 assert_eq!(fee_updates.refund, token::BaseUnits::default());
215
216 let block_fees = mgr.commit_block();
217 assert!(block_fees.is_empty(), "there should be no recorded fees");
218 }
219
220 #[test]
221 #[should_panic(expected = "transaction fee payer cannot change")]
222 fn test_fail_payer_change() {
223 let mut mgr = FeeManager::new();
224
225 let fee = token::BaseUnits::native(1_000_000);
226 mgr.record_fee(keys::alice::address(), &fee);
227 mgr.record_fee(keys::bob::address(), &fee); }
229
230 #[test]
231 #[should_panic(expected = "transaction fee denomination cannot change")]
232 fn test_fail_denomination_change() {
233 let mut mgr = FeeManager::new();
234
235 let fee = token::BaseUnits::native(1_000_000);
236 mgr.record_fee(keys::alice::address(), &fee);
237
238 let fee = token::BaseUnits::new(1_000_000, "TEST".parse().unwrap());
239 mgr.record_fee(keys::alice::address(), &fee); }
241}