penumbra_sdk_fee/component/
fee_pay.rs

1use anyhow::{ensure, Result};
2use async_trait::async_trait;
3use cnidarium::StateWrite;
4use penumbra_sdk_asset::Value;
5use penumbra_sdk_proto::core::component::fee::v1 as pb;
6use penumbra_sdk_proto::state::StateWriteProto as _;
7
8use crate::{Fee, Gas};
9
10use super::view::{StateReadExt, StateWriteExt};
11
12/// Allows payment of transaction fees.
13#[async_trait]
14pub trait FeePay: StateWrite {
15    /// Uses the provided `fee` to pay for `gas_used`, erroring if the fee is insufficient.
16    async fn pay_fee(&mut self, gas_used: Gas, fee: Fee) -> Result<()> {
17        let current_gas_prices = if fee.asset_id() == *penumbra_sdk_asset::STAKING_TOKEN_ASSET_ID {
18            self.get_gas_prices()
19                .await
20                .expect("gas prices must be present in state")
21        } else {
22            let alt_gas_prices = self
23                .get_alt_gas_prices()
24                .await
25                .expect("alt gas prices must be present in state");
26            // This does a linear scan, but we think that's OK because we're expecting
27            // a small number of alt gas prices before switching to the DEX directly.
28            alt_gas_prices
29                .into_iter()
30                .find(|prices| prices.asset_id == fee.asset_id())
31                .ok_or_else(|| {
32                    anyhow::anyhow!("fee token {} not recognized by the chain", fee.asset_id())
33                })?
34        };
35
36        // Double check that the gas price assets match.
37        ensure!(
38            current_gas_prices.asset_id == fee.asset_id(),
39            "unexpected mismatch between fee and queried gas prices (expected: {}, found: {})",
40            fee.asset_id(),
41            current_gas_prices.asset_id,
42        );
43
44        // Compute the base fee for the `gas_used`.
45        let base_fee = current_gas_prices.fee(&gas_used);
46
47        // The provided fee must be at least the base fee.
48        ensure!(
49            fee.amount() >= base_fee.amount(),
50            "fee must be greater than or equal to the transaction base price (supplied: {}, base: {})",
51            fee.amount(),
52            base_fee.amount(),
53        );
54
55        // Otherwise, the fee less the base fee is the proposer tip.
56        let tip = Fee(Value {
57            amount: fee.amount() - base_fee.amount(),
58            asset_id: fee.asset_id(),
59        });
60
61        // Record information about the fee payment in an event.
62        self.record_proto(pb::EventPaidFee {
63            fee: Some(fee.into()),
64            base_fee: Some(base_fee.into()),
65            gas_used: Some(gas_used.into()),
66            tip: Some(tip.into()),
67        });
68
69        // Finally, queue the paid fee for processing at the end of the block.
70        self.raw_accumulate_base_fee(base_fee);
71        self.raw_accumulate_tip(tip);
72
73        Ok(())
74    }
75}
76
77impl<S: StateWrite + ?Sized> FeePay for S {}