Skip to main content

cast/cmd/
storage_credits.rs

1use crate::{
2    cmd::tip20::send_tip20_transaction,
3    tempo::{ensure_tempo_precompile_active, print_payload, tempo_provider},
4    tx::{SendTxOpts, TxParams},
5};
6use alloy_ens::NameOrAddress;
7use alloy_primitives::Address;
8use alloy_sol_types::SolCall;
9use clap::{Parser, ValueEnum};
10use eyre::Result;
11use foundry_cli::opts::RpcOpts;
12use foundry_common::provider::RetryProvider;
13use foundry_evm::hardfork::TempoHardfork;
14use serde_json::{Value, json};
15use std::str::FromStr;
16use tempo_alloy::TempoNetwork;
17use tempo_contracts::precompiles::{IStorageCredits, STORAGE_CREDITS_ADDRESS};
18
19/// T7 storage credits operations (Tempo).
20///
21/// Storage credits are a per-account, non-transferable balance minted when an account frees its own
22/// storage and later spent to discount the creation cost of new storage. This wraps the T7
23/// StorageCredits precompile at `0x1060000000000000000000000000000000000000`.
24#[derive(Debug, Parser, Clone)]
25pub enum StorageCreditsSubcommand {
26    /// Show an account's storage credit balance.
27    Balance {
28        /// Account to query.
29        #[arg(value_parser = NameOrAddress::from_str)]
30        account: NameOrAddress,
31
32        #[command(flatten)]
33        rpc: RpcOpts,
34    },
35
36    /// Show an account's storage credit consumption mode.
37    ///
38    /// Mode is transaction-local transient state, so a standalone read reflects the default rather
39    /// than a value set by an earlier `set-mode` transaction.
40    Mode {
41        /// Account to query.
42        #[arg(value_parser = NameOrAddress::from_str)]
43        account: NameOrAddress,
44
45        #[command(flatten)]
46        rpc: RpcOpts,
47    },
48
49    /// Show an account's storage credit spend budget.
50    ///
51    /// Budget is transaction-local transient state, so a standalone read reflects the default
52    /// rather than a value set by an earlier `set-budget` transaction.
53    Budget {
54        /// Account to query.
55        #[arg(value_parser = NameOrAddress::from_str)]
56        account: NameOrAddress,
57
58        #[command(flatten)]
59        rpc: RpcOpts,
60    },
61
62    /// Set the caller's storage credit consumption mode.
63    ///
64    /// The mode only applies within the transaction that sets it; batch it with the storage
65    /// operations it should govern.
66    SetMode {
67        /// Mode to switch to.
68        #[arg(value_enum)]
69        mode: CreditMode,
70
71        #[command(flatten)]
72        send_tx: SendTxOpts,
73
74        #[command(flatten)]
75        tx: TxParams,
76    },
77
78    /// Set the caller's storage credit spend budget, which also selects `direct` mode.
79    ///
80    /// The budget only applies within the transaction that sets it; batch it with the storage
81    /// operations it should govern.
82    SetBudget {
83        /// Maximum number of credits the caller may spend in `direct` mode this transaction.
84        credits: u64,
85
86        #[command(flatten)]
87        send_tx: SendTxOpts,
88
89        #[command(flatten)]
90        tx: TxParams,
91    },
92}
93
94/// CLI-facing spelling of `IStorageCredits::Mode`.
95#[derive(Debug, Clone, Copy, ValueEnum)]
96pub enum CreditMode {
97    /// Pay creation cost upfront, then settle credits as a refund at end of transaction.
98    Refund,
99    /// Pay creation cost upfront and keep freed credits instead of spending them.
100    Preserve,
101    /// Spend existing credits synchronously; selecting this sets an effectively unlimited budget.
102    Direct,
103}
104
105impl StorageCreditsSubcommand {
106    pub async fn run(self) -> Result<()> {
107        match self {
108            Self::Balance { account, rpc } => {
109                read(account, rpc, "balance", "Balance:", |credits, account| async move {
110                    Ok(json!(credits.balanceOf(account).call().await?))
111                })
112                .await
113            }
114            Self::Mode { account, rpc } => {
115                read(account, rpc, "mode", "Mode:   ", |credits, account| async move {
116                    Ok(json!(credits.modeOf(account).call().await?.as_str()))
117                })
118                .await
119            }
120            Self::Budget { account, rpc } => {
121                read(account, rpc, "budget", "Budget: ", |credits, account| async move {
122                    Ok(json!(credits.budgetOf(account).call().await?))
123                })
124                .await
125            }
126            Self::SetMode { mode, send_tx, tx } => {
127                ensure_t7(&send_tx.eth.rpc, "cast storage-credits set-mode").await?;
128                let new_mode = match mode {
129                    CreditMode::Refund => IStorageCredits::Mode::Refund,
130                    CreditMode::Preserve => IStorageCredits::Mode::Preserve,
131                    CreditMode::Direct => IStorageCredits::Mode::Direct,
132                };
133                let data = IStorageCredits::setModeCall { newMode: new_mode }.abi_encode();
134                send_tip20_transaction(STORAGE_CREDITS_ADDRESS, data, send_tx, tx).await
135            }
136            Self::SetBudget { credits, send_tx, tx } => {
137                ensure_t7(&send_tx.eth.rpc, "cast storage-credits set-budget").await?;
138                let data = IStorageCredits::setBudgetCall { credits }.abi_encode();
139                send_tip20_transaction(STORAGE_CREDITS_ADDRESS, data, send_tx, tx).await
140            }
141        }
142    }
143}
144
145type Credits = IStorageCredits::IStorageCreditsInstance<RetryProvider<TempoNetwork>, TempoNetwork>;
146
147/// Reads one account field from the precompile and prints it as `key` in JSON mode and after
148/// `label` otherwise.
149async fn read<F, Fut>(
150    account: NameOrAddress,
151    rpc: RpcOpts,
152    key: &str,
153    label: &str,
154    query: F,
155) -> Result<()>
156where
157    F: FnOnce(Credits, Address) -> Fut,
158    Fut: Future<Output = Result<Value>>,
159{
160    let provider = ensure_t7(&rpc, &format!("cast storage-credits {key}")).await?;
161    let account = account.resolve(&provider).await?;
162    let value = query(IStorageCredits::new(STORAGE_CREDITS_ADDRESS, provider), account).await?;
163    let payload = json!({ "account": format!("{account}"), key: value });
164    print_payload(payload, |payload| {
165        let value = &payload[key];
166        let value = value.as_str().map_or_else(|| value.to_string(), str::to_string);
167        sh_println!("Account: {}\n{label} {value}", payload["account"].as_str().unwrap_or_default())
168    })
169}
170
171/// The StorageCredits precompile only exists on T7+; fail early with a clear message instead of
172/// surfacing a raw revert (or, for writes, a silently successful no-op transaction to an empty
173/// account).
174async fn ensure_t7(rpc: &RpcOpts, command: &str) -> Result<RetryProvider<TempoNetwork>> {
175    let (_, provider) = tempo_provider(rpc)?;
176    ensure_tempo_precompile_active(
177        &provider,
178        TempoHardfork::T7,
179        STORAGE_CREDITS_ADDRESS,
180        &format!("{command} requires a Tempo T7-capable StorageCredits RPC"),
181    )
182    .await?;
183    Ok(provider)
184}