Skip to main content

cast/cmd/
storage_credits.rs

1use crate::{
2    cmd::{
3        keychain::ensure_tempo_precompile_active,
4        tip20::{resolve_tip20_signer, send_tip20_transaction},
5    },
6    tempo::print_payload,
7    tx::{SendTxOpts, TxParams},
8};
9use alloy_ens::NameOrAddress;
10use clap::{Parser, ValueEnum};
11use eyre::Result;
12use foundry_cli::{opts::RpcOpts, utils::LoadConfig};
13use foundry_common::provider::ProviderBuilder;
14use foundry_evm::hardfork::TempoHardfork;
15use serde_json::json;
16use std::str::FromStr;
17use tempo_alloy::TempoNetwork;
18use tempo_contracts::precompiles::{IStorageCredits, STORAGE_CREDITS_ADDRESS};
19
20/// T7 storage credits operations (Tempo).
21///
22/// Storage credits are a per-account, non-transferable balance minted when an account frees its own
23/// storage and later spent to discount the creation cost of new storage. This wraps the T7
24/// StorageCredits precompile at `0x1060000000000000000000000000000000000000`.
25#[derive(Debug, Parser, Clone)]
26pub enum StorageCreditsSubcommand {
27    /// Show an account's storage credit balance.
28    Balance {
29        /// Account to query.
30        #[arg(value_parser = NameOrAddress::from_str)]
31        account: NameOrAddress,
32
33        #[command(flatten)]
34        rpc: RpcOpts,
35    },
36
37    /// Show an account's storage credit consumption mode.
38    ///
39    /// Mode is transaction-local transient state, so a standalone read reflects the default rather
40    /// than a value set by an earlier `set-mode` transaction.
41    Mode {
42        /// Account to query.
43        #[arg(value_parser = NameOrAddress::from_str)]
44        account: NameOrAddress,
45
46        #[command(flatten)]
47        rpc: RpcOpts,
48    },
49
50    /// Show an account's storage credit spend budget.
51    ///
52    /// Budget is transaction-local transient state, so a standalone read reflects the default
53    /// rather than a value set by an earlier `set-budget` transaction.
54    Budget {
55        /// Account to query.
56        #[arg(value_parser = NameOrAddress::from_str)]
57        account: NameOrAddress,
58
59        #[command(flatten)]
60        rpc: RpcOpts,
61    },
62
63    /// Set the caller's storage credit consumption mode.
64    ///
65    /// The mode only applies within the transaction that sets it; batch it with the storage
66    /// operations it should govern.
67    SetMode {
68        /// Mode to switch to.
69        #[arg(value_enum)]
70        mode: CreditMode,
71
72        #[command(flatten)]
73        send_tx: SendTxOpts,
74
75        #[command(flatten)]
76        tx: TxParams,
77    },
78
79    /// Set the caller's storage credit spend budget, which also selects `direct` mode.
80    ///
81    /// The budget only applies within the transaction that sets it; batch it with the storage
82    /// operations it should govern.
83    SetBudget {
84        /// Maximum number of credits the caller may spend in `direct` mode this transaction.
85        credits: u64,
86
87        #[command(flatten)]
88        send_tx: SendTxOpts,
89
90        #[command(flatten)]
91        tx: TxParams,
92    },
93}
94
95/// CLI-facing spelling of `IStorageCredits::Mode`.
96#[derive(Debug, Clone, Copy, ValueEnum)]
97pub enum CreditMode {
98    /// Pay creation cost upfront, then settle credits as a refund at end of transaction.
99    Refund,
100    /// Pay creation cost upfront and keep freed credits instead of spending them.
101    Preserve,
102    /// Spend existing credits synchronously; selecting this sets an effectively unlimited budget.
103    Direct,
104}
105
106impl StorageCreditsSubcommand {
107    pub async fn run(self) -> Result<()> {
108        match self {
109            Self::Balance { account, rpc } => balance(account, rpc).await,
110            Self::Mode { account, rpc } => mode(account, rpc).await,
111            Self::Budget { account, rpc } => budget(account, rpc).await,
112            Self::SetMode { mode, send_tx, tx } => set_mode(mode, send_tx, tx).await,
113            Self::SetBudget { credits, send_tx, tx } => set_budget(credits, send_tx, tx).await,
114        }
115    }
116}
117
118async fn balance(account: NameOrAddress, rpc: RpcOpts) -> Result<()> {
119    let config = rpc.load_config()?;
120    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
121    ensure_storage_credits_t7(&provider, "cast storage-credits balance").await?;
122    let account = account.resolve(&provider).await?;
123
124    let credits = IStorageCredits::new(STORAGE_CREDITS_ADDRESS, &provider);
125    let balance = credits.balanceOf(account).call().await?;
126    let payload = json!({ "account": format!("{account}"), "balance": balance });
127    print_payload(payload, |payload| {
128        sh_println!(
129            "Account: {}\nBalance: {}",
130            payload["account"].as_str().unwrap_or_default(),
131            payload["balance"],
132        )
133    })
134}
135
136async fn mode(account: NameOrAddress, rpc: RpcOpts) -> Result<()> {
137    let config = rpc.load_config()?;
138    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
139    ensure_storage_credits_t7(&provider, "cast storage-credits mode").await?;
140    let account = account.resolve(&provider).await?;
141
142    let credits = IStorageCredits::new(STORAGE_CREDITS_ADDRESS, &provider);
143    let mode = credits.modeOf(account).call().await?;
144    let payload = json!({ "account": format!("{account}"), "mode": mode.as_str() });
145    print_payload(payload, |payload| {
146        sh_println!(
147            "Account: {}\nMode:    {}",
148            payload["account"].as_str().unwrap_or_default(),
149            payload["mode"].as_str().unwrap_or_default(),
150        )
151    })
152}
153
154async fn budget(account: NameOrAddress, rpc: RpcOpts) -> Result<()> {
155    let config = rpc.load_config()?;
156    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
157    ensure_storage_credits_t7(&provider, "cast storage-credits budget").await?;
158    let account = account.resolve(&provider).await?;
159
160    let credits = IStorageCredits::new(STORAGE_CREDITS_ADDRESS, &provider);
161    let budget = credits.budgetOf(account).call().await?;
162    let payload = json!({ "account": format!("{account}"), "budget": budget });
163    print_payload(payload, |payload| {
164        sh_println!(
165            "Account: {}\nBudget:  {}",
166            payload["account"].as_str().unwrap_or_default(),
167            payload["budget"],
168        )
169    })
170}
171
172async fn set_mode(mode: CreditMode, send_tx: SendTxOpts, tx: TxParams) -> Result<()> {
173    ensure_send_storage_credits_t7(&send_tx, "cast storage-credits set-mode").await?;
174    let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
175    // The precompile ABI encodes `Mode` as its `uint8` discriminant.
176    let mode_arg = (mode.to_sol() as u8).to_string();
177    send_tip20_transaction(
178        NameOrAddress::Address(STORAGE_CREDITS_ADDRESS),
179        "setMode(uint8)",
180        vec![mode_arg],
181        send_tx,
182        tx,
183        signer,
184        access_key,
185    )
186    .await
187}
188
189async fn set_budget(credits: u64, send_tx: SendTxOpts, tx: TxParams) -> Result<()> {
190    ensure_send_storage_credits_t7(&send_tx, "cast storage-credits set-budget").await?;
191    let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
192    send_tip20_transaction(
193        NameOrAddress::Address(STORAGE_CREDITS_ADDRESS),
194        "setBudget(uint64)",
195        vec![credits.to_string()],
196        send_tx,
197        tx,
198        signer,
199        access_key,
200    )
201    .await
202}
203
204/// The StorageCredits precompile only exists on T7+; fail early with a clear message instead of
205/// surfacing a raw revert. Fall back to a code check when the RPC lacks the hardfork query.
206async fn ensure_storage_credits_t7<P>(provider: &P, command: &str) -> Result<()>
207where
208    P: alloy_provider::Provider<TempoNetwork>,
209{
210    ensure_tempo_precompile_active(
211        provider,
212        TempoHardfork::T7,
213        STORAGE_CREDITS_ADDRESS,
214        &format!("{command} requires a Tempo T7-capable StorageCredits RPC"),
215    )
216    .await
217}
218
219/// Gate a write command on T7 before signing: on pre-T7 the precompile address is an empty account,
220/// so a transaction to it would silently succeed as a no-op.
221async fn ensure_send_storage_credits_t7(send_tx: &SendTxOpts, command: &str) -> Result<()> {
222    let config = send_tx.eth.rpc.load_config()?;
223    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
224    ensure_storage_credits_t7(&provider, command).await
225}
226
227impl CreditMode {
228    const fn to_sol(self) -> IStorageCredits::Mode {
229        match self {
230            Self::Refund => IStorageCredits::Mode::Refund,
231            Self::Preserve => IStorageCredits::Mode::Preserve,
232            Self::Direct => IStorageCredits::Mode::Direct,
233        }
234    }
235}