1use anyhow::Context;
2use penumbra_sdk_proto::{penumbra::core::component::fee::v1 as pb, DomainType};
3use std::fmt;
4use std::str::FromStr;
5
6use decaf377::Fr;
7use penumbra_sdk_asset::{asset, balance, Balance, Value, STAKING_TOKEN_ASSET_ID};
8use penumbra_sdk_num::Amount;
9
10pub static FEE_TIER_LOW_MULTIPLIER: u32 = 105;
12pub static FEE_TIER_MEDIUM_MULTIPLIER: u32 = 130;
13pub static FEE_TIER_HIGH_MULTIPLIER: u32 = 200;
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub struct Fee(pub Value);
17
18impl Default for Fee {
19 fn default() -> Self {
20 Fee::from_staking_token_amount(Amount::zero())
21 }
22}
23
24impl Fee {
25 pub fn from_staking_token_amount(amount: Amount) -> Self {
26 Self(Value {
27 amount,
28 asset_id: *STAKING_TOKEN_ASSET_ID,
29 })
30 }
31
32 pub fn amount(&self) -> Amount {
33 self.0.amount
34 }
35
36 pub fn asset_id(&self) -> asset::Id {
37 self.0.asset_id
38 }
39
40 pub fn asset_matches(&self, other: &Fee) -> bool {
41 self.asset_id() == other.asset_id()
42 }
43
44 pub fn balance(&self) -> balance::Balance {
45 -Balance::from(self.0)
46 }
47
48 pub fn commit(&self, blinding: Fr) -> balance::Commitment {
49 self.balance().commit(blinding)
50 }
51
52 pub fn format(&self, cache: &asset::Cache) -> String {
53 self.0.format(cache)
54 }
55
56 pub fn apply_tier(self, fee_tier: FeeTier) -> Self {
57 match fee_tier {
60 FeeTier::Low => {
61 let amount = (self.amount() * FEE_TIER_LOW_MULTIPLIER.into()) / 100u32.into();
62 Self(Value {
63 amount,
64 asset_id: self.0.asset_id,
65 })
66 }
67 FeeTier::Medium => {
68 let amount = (self.amount() * FEE_TIER_MEDIUM_MULTIPLIER.into()) / 100u32.into();
69 Self(Value {
70 amount,
71 asset_id: self.0.asset_id,
72 })
73 }
74 FeeTier::High => {
75 let amount = (self.amount() * FEE_TIER_HIGH_MULTIPLIER.into()) / 100u32.into();
76 Self(Value {
77 amount,
78 asset_id: self.0.asset_id,
79 })
80 }
81 }
82 }
83}
84
85impl DomainType for Fee {
86 type Proto = pb::Fee;
87}
88
89impl From<Fee> for pb::Fee {
90 fn from(fee: Fee) -> Self {
91 if fee.0.asset_id == *STAKING_TOKEN_ASSET_ID {
92 pb::Fee {
93 amount: Some(fee.0.amount.into()),
94 asset_id: None,
95 }
96 } else {
97 pb::Fee {
98 amount: Some(fee.0.amount.into()),
99 asset_id: Some(fee.0.asset_id.into()),
100 }
101 }
102 }
103}
104
105impl TryFrom<pb::Fee> for Fee {
106 type Error = anyhow::Error;
107
108 fn try_from(proto: pb::Fee) -> anyhow::Result<Self> {
109 if proto.asset_id.is_some() {
110 Ok(Fee(Value {
111 amount: proto
112 .amount
113 .context("missing protobuf contents for Fee Amount")?
114 .try_into()?,
115 asset_id: proto
116 .asset_id
117 .context("missing protobuf contents for Fee Asset ID")?
118 .try_into()?,
119 }))
120 } else {
121 Ok(Fee(Value {
122 amount: proto
123 .amount
124 .context("missing protobuf contents for Fee Amount")?
125 .try_into()?,
126 asset_id: *STAKING_TOKEN_ASSET_ID,
127 }))
128 }
129 }
130}
131
132impl Fee {
133 pub fn value(&self) -> Value {
134 self.0
135 }
136}
137
138#[derive(Copy, Clone, Debug)]
139pub enum FeeTier {
140 Low,
141 Medium,
142 High,
143}
144
145impl Default for FeeTier {
146 fn default() -> Self {
147 Self::Low
148 }
149}
150
151impl fmt::Display for FeeTier {
152 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153 let s = match self {
154 FeeTier::Low => "low".to_owned(),
155 FeeTier::Medium => "medium".to_owned(),
156 FeeTier::High => "high".to_owned(),
157 };
158 write!(f, "{}", s)
159 }
160}
161
162impl FromStr for FeeTier {
163 type Err = anyhow::Error;
164 fn from_str(s: &str) -> Result<Self, Self::Err> {
165 match s {
166 "low" => Ok(FeeTier::Low),
167 "medium" => Ok(FeeTier::Medium),
168 "high" => Ok(FeeTier::High),
169 _ => anyhow::bail!(format!("cannot parse '{}' as FeeTier", s)),
170 }
171 }
172}
173
174impl DomainType for FeeTier {
175 type Proto = pb::FeeTier;
176}
177
178impl From<FeeTier> for pb::FeeTier {
179 fn from(prices: FeeTier) -> Self {
180 match prices {
181 FeeTier::Low => pb::FeeTier {
182 fee_tier: pb::fee_tier::Tier::Low.into(),
183 },
184 FeeTier::Medium => pb::FeeTier {
185 fee_tier: pb::fee_tier::Tier::Medium.into(),
186 },
187 FeeTier::High => pb::FeeTier {
188 fee_tier: pb::fee_tier::Tier::High.into(),
189 },
190 }
191 }
192}
193
194impl TryFrom<pb::FeeTier> for FeeTier {
195 type Error = anyhow::Error;
196
197 fn try_from(proto: pb::FeeTier) -> Result<Self, Self::Error> {
198 match pb::fee_tier::Tier::try_from(proto.fee_tier)? {
199 pb::fee_tier::Tier::Low => Ok(FeeTier::Low),
200 pb::fee_tier::Tier::Medium => Ok(FeeTier::Medium),
201 pb::fee_tier::Tier::High => Ok(FeeTier::High),
202 _ => Err(anyhow::anyhow!("invalid fee tier")),
203 }
204 }
205}