penumbra_sdk_fee/
fee.rs

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
10// Each fee tier multiplier has an implicit 100 denominator.
11pub 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        // TODO: this could be fingerprinted since fees are public; it would be ideal to apply
58        // some sampling distribution, see https://github.com/penumbra-zone/penumbra/issues/3153
59        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}