staking_cli/
lib.rs

1use alloy::{
2    eips::BlockId,
3    network::EthereumWallet,
4    primitives::{utils::parse_ether, Address, U256},
5    signers::local::{coins_bip39::English, MnemonicBuilder},
6};
7use anyhow::{bail, Result};
8use clap::{Args as ClapArgs, Parser, Subcommand};
9use clap_serde_derive::ClapSerde;
10use demo::DelegationConfig;
11use espresso_contract_deployer::provider::connect_ledger;
12pub(crate) use hotshot_types::{light_client::StateSignKey, signature_key::BLSPrivKey};
13pub(crate) use jf_signature::bls_over_bn254::KeyPair as BLSKeyPair;
14use metadata::MetadataUri;
15use parse::Commission;
16use sequencer_utils::logging;
17use serde::{Deserialize, Serialize};
18use url::Url;
19
20pub mod claim;
21pub mod delegation;
22pub mod demo;
23pub mod funding;
24pub mod info;
25pub mod l1;
26pub mod metadata;
27pub mod output;
28pub mod parse;
29pub mod receipt;
30pub mod registration;
31pub mod signature;
32
33#[cfg(feature = "testing")]
34pub mod deploy;
35
36pub const DEV_MNEMONIC: &str = "test test test test test test test test test test test junk";
37
38/// CLI to interact with the Espresso stake table contract
39#[derive(ClapSerde, Clone, Debug, Deserialize, Serialize)]
40#[command(version, about, long_about = None)]
41pub struct Config {
42    /// L1 Ethereum RPC.
43    #[clap(long, env = "L1_PROVIDER")]
44    #[default(Url::parse("http://localhost:8545").unwrap())]
45    pub rpc_url: Url,
46
47    /// [DEPRECATED] Deployed ESP token contract address.
48    ///
49    /// [DEPRECATED] This is fetched from the stake table contract now.
50    #[clap(long, env = "ESP_TOKEN_ADDRESS")]
51    pub token_address: Option<Address>,
52
53    /// Deployed stake table contract address.
54    #[clap(long, env = "STAKE_TABLE_ADDRESS")]
55    pub stake_table_address: Address,
56
57    /// Espresso sequencer API URL for reward claims.
58    #[clap(long, env = "ESPRESSO_URL")]
59    pub espresso_url: Option<Url>,
60
61    #[clap(flatten)]
62    pub signer: SignerConfig,
63
64    #[clap(flatten)]
65    #[serde(skip)]
66    pub logging: logging::Config,
67
68    #[command(subcommand)]
69    #[serde(skip)]
70    pub commands: Commands,
71}
72
73#[derive(ClapSerde, Parser, Clone, Debug, Deserialize, Serialize)]
74pub struct SignerConfig {
75    /// The mnemonic to use when deriving the key.
76    #[clap(long, env = "MNEMONIC")]
77    pub mnemonic: Option<String>,
78
79    /// The mnemonic account index to use when deriving the key.
80    #[clap(long, env = "ACCOUNT_INDEX")]
81    #[default(Some(0))]
82    pub account_index: Option<u32>,
83
84    /// Use a ledger device to sign transactions.
85    ///
86    /// NOTE: ledger must be unlocked, Ethereum app open and blind signing must be enabled in the
87    /// Ethereum app settings.
88    #[clap(long, env = "USE_LEDGER")]
89    pub ledger: bool,
90}
91
92#[derive(Clone, Debug)]
93pub enum ValidSignerConfig {
94    Mnemonic {
95        mnemonic: String,
96        account_index: u32,
97    },
98    Ledger {
99        account_index: usize,
100    },
101}
102
103impl TryFrom<SignerConfig> for ValidSignerConfig {
104    type Error = anyhow::Error;
105
106    fn try_from(config: SignerConfig) -> Result<Self> {
107        let account_index = config
108            .account_index
109            .ok_or_else(|| anyhow::anyhow!("Account index must be provided"))?;
110        if let Some(mnemonic) = config.mnemonic {
111            Ok(ValidSignerConfig::Mnemonic {
112                mnemonic,
113                account_index,
114            })
115        } else if config.ledger {
116            Ok(ValidSignerConfig::Ledger {
117                account_index: account_index as usize,
118            })
119        } else {
120            bail!("Either mnemonic or --ledger flag must be provided")
121        }
122    }
123}
124
125impl ValidSignerConfig {
126    pub async fn wallet(&self) -> Result<(EthereumWallet, Address)> {
127        match self {
128            ValidSignerConfig::Mnemonic {
129                mnemonic,
130                account_index,
131            } => {
132                let signer = MnemonicBuilder::<English>::default()
133                    .phrase(mnemonic)
134                    .index(*account_index)?
135                    .build()?;
136                let account = signer.address();
137                let wallet = EthereumWallet::from(signer);
138                Ok((wallet, account))
139            },
140            ValidSignerConfig::Ledger { account_index } => {
141                let signer = connect_ledger(*account_index).await?;
142                let account = signer.get_address().await?;
143                let wallet = EthereumWallet::from(signer);
144                Ok((wallet, account))
145            },
146        }
147    }
148}
149
150#[derive(ClapArgs, Debug, Clone)]
151#[group(required = true, multiple = false)]
152pub struct MetadataUriArgs {
153    #[clap(long, env = "METADATA_URI")]
154    metadata_uri: Option<String>,
155
156    #[clap(long, env = "NO_METADATA_URI")]
157    no_metadata_uri: bool,
158}
159
160impl TryFrom<MetadataUriArgs> for MetadataUri {
161    type Error = anyhow::Error;
162
163    fn try_from(args: MetadataUriArgs) -> Result<Self> {
164        if args.no_metadata_uri {
165            Ok(MetadataUri::empty())
166        } else if let Some(uri_str) = args.metadata_uri {
167            uri_str.parse()
168        } else {
169            bail!("Either --metadata-uri or --no-metadata-uri must be provided")
170        }
171    }
172}
173
174impl Default for Commands {
175    fn default() -> Self {
176        Commands::StakeTable {
177            l1_block_number: None,
178            compact: false,
179        }
180    }
181}
182
183impl Config {
184    pub fn apply_env_var_overrides(self) -> Result<Self> {
185        let mut config = self.clone();
186        if self.stake_table_address == Address::ZERO {
187            let stake_table_env_var = "ESPRESSO_SEQUENCER_STAKE_TABLE_PROXY_ADDRESS";
188            if let Ok(stake_table_address) = std::env::var(stake_table_env_var) {
189                config.stake_table_address = stake_table_address.parse()?;
190                tracing::info!(
191                    "Using stake table address from env {stake_table_env_var}: \
192                     {stake_table_address}",
193                );
194            }
195        }
196        Ok(config)
197    }
198}
199
200#[derive(Subcommand, Debug, Clone)]
201pub enum Commands {
202    /// Display version information of the staking-cli.
203    Version,
204    /// Display the current configuration
205    Config,
206    /// Initialize the config file with deployment and wallet info.
207    Init {
208        /// The mnemonic to use when deriving the key.
209        #[clap(long, env = "MNEMONIC", required_unless_present = "ledger")]
210        mnemonic: Option<String>,
211
212        /// The mnemonic account index to use when deriving the key.
213        #[clap(long, env = "ACCOUNT_INDEX", default_value_t = 0)]
214        account_index: u32,
215
216        /// The ledger account index to use when deriving the key.
217        #[clap(long, env = "LEDGER_INDEX", required_unless_present = "mnemonic")]
218        ledger: bool,
219    },
220    /// Remove the config file.
221    Purge {
222        /// Don't ask for confirmation.
223        #[clap(long)]
224        force: bool,
225    },
226    /// Show the stake table in the Espresso stake table contract.
227    StakeTable {
228        /// The block numberto use for the stake table.
229        ///
230        /// Defaults to the latest block for convenience.
231        #[clap(long)]
232        l1_block_number: Option<BlockId>,
233
234        /// Abbreviate the very long BLS public keys.
235        #[clap(long)]
236        compact: bool,
237    },
238    /// Print the signer account address.
239    Account,
240    /// Register to become a validator.
241    RegisterValidator {
242        #[clap(flatten)]
243        signature_args: signature::NodeSignatureArgs,
244
245        /// The commission to charge delegators
246        #[clap(long, value_parser = parse::parse_commission, env = "COMMISSION")]
247        commission: Commission,
248
249        #[clap(flatten)]
250        metadata_uri_args: MetadataUriArgs,
251    },
252    /// Update a validators Espresso consensus signing keys.
253    UpdateConsensusKeys {
254        #[clap(flatten)]
255        signature_args: signature::NodeSignatureArgs,
256    },
257    /// Deregister a validator.
258    DeregisterValidator {},
259    /// Update validator commission rate.
260    UpdateCommission {
261        /// The new commission rate to set
262        #[clap(long, value_parser = parse::parse_commission, env = "NEW_COMMISSION")]
263        new_commission: Commission,
264    },
265    /// Update validator metadata URL.
266    UpdateMetadataUri {
267        #[clap(flatten)]
268        metadata_uri_args: MetadataUriArgs,
269    },
270    /// Approve stake table contract to move tokens
271    Approve {
272        #[clap(long, value_parser = parse_ether)]
273        amount: U256,
274    },
275    /// Delegate funds to a validator.
276    Delegate {
277        #[clap(long)]
278        validator_address: Address,
279
280        #[clap(long, value_parser = parse_ether)]
281        amount: U256,
282    },
283    /// Initiate a withdrawal of delegated funds from a validator.
284    Undelegate {
285        #[clap(long)]
286        validator_address: Address,
287
288        #[clap(long, value_parser = parse_ether)]
289        amount: U256,
290    },
291    /// Claim withdrawal after an undelegation.
292    ClaimWithdrawal {
293        #[clap(long)]
294        validator_address: Address,
295    },
296    /// Claim withdrawal after validator exit.
297    ClaimValidatorExit {
298        #[clap(long)]
299        validator_address: Address,
300    },
301    /// Claim staking rewards.
302    ClaimRewards,
303    /// Check unclaimed staking rewards.
304    UnclaimedRewards {
305        /// The address to check.
306        #[clap(long)]
307        address: Option<Address>,
308    },
309    /// Check ESP token balance.
310    TokenBalance {
311        /// The address to check.
312        #[clap(long)]
313        address: Option<Address>,
314    },
315    /// Check ESP token allowance of stake table contract.
316    TokenAllowance {
317        /// The address to check.
318        #[clap(long)]
319        owner: Option<Address>,
320    },
321    /// Transfer ESP tokens
322    Transfer {
323        /// The address to transfer to.
324        #[clap(long)]
325        to: Address,
326
327        /// The amount to transfer
328        #[clap(long, value_parser = parse_ether)]
329        amount: U256,
330    },
331    /// Register the validators and delegates for the local demo.
332    StakeForDemo {
333        /// The number of validators to register.
334        ///
335        /// The default (5) works for the local native and docker demos.
336        #[clap(long, default_value_t = 5)]
337        num_validators: u16,
338
339        /// The number of delegators to create per validator.
340        ///
341        /// If not specified, a random number (2-5) of delegators is created per validator.
342        /// Must be <= 100,000.
343        #[clap(long, env = "NUM_DELEGATORS_PER_VALIDATOR", value_parser = clap::value_parser!(u64).range(..=100000))]
344        num_delegators_per_validator: Option<u64>,
345
346        #[arg(long, value_enum, env = "DELEGATION_CONFIG", default_value_t = DelegationConfig::default())]
347        delegation_config: DelegationConfig,
348    },
349    /// Export validator node signatures for address validation.
350    ExportNodeSignatures {
351        /// The Ethereum address to sign.
352        #[clap(long)]
353        address: Address,
354
355        /// The BLS private key for signing.
356        #[clap(long, value_parser = parse::parse_bls_priv_key, env = "BLS_PRIVATE_KEY")]
357        consensus_private_key: BLSPrivKey,
358
359        /// The Schnorr private key for signing.
360        #[clap(long, value_parser = parse::parse_state_priv_key, env = "SCHNORR_PRIVATE_KEY")]
361        state_private_key: StateSignKey,
362
363        #[clap(flatten)]
364        output_args: signature::OutputArgs,
365    },
366}