staking_cli/
demo.rs

1use std::{
2    collections::{HashMap, HashSet, VecDeque},
3    fmt,
4};
5
6use alloy::{
7    contract::Error as ContractError,
8    network::{Ethereum, EthereumWallet},
9    primitives::{
10        utils::{format_ether, parse_ether},
11        Address, U256,
12    },
13    providers::{PendingTransactionBuilder, Provider, ProviderBuilder, WalletProvider},
14    rpc::{client::RpcClient, types::TransactionReceipt},
15    signers::local::PrivateKeySigner,
16    transports::{http::Http, TransportError},
17};
18use anyhow::Result;
19use clap::ValueEnum;
20use espresso_contract_deployer::{build_provider, build_signer, HttpProviderWithWallet};
21use futures_util::future;
22use hotshot_contract_adapter::{
23    sol_types::{EspToken, StakeTableV2},
24    stake_table::StakeTableContractVersion,
25};
26use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
27use rand::{Rng, SeedableRng};
28use rand_chacha::ChaCha20Rng;
29use thiserror::Error;
30use url::Url;
31
32use crate::{
33    delegation::{approve, delegate},
34    funding::{send_esp, send_eth},
35    info::fetch_token_address,
36    parse::{parse_bls_priv_key, parse_state_priv_key, Commission, ParseCommissionError},
37    receipt::ReceiptExt as _,
38    registration::register_validator,
39    signature::NodeSignatures,
40    Config,
41};
42
43#[derive(Debug, Error)]
44pub enum CreateTransactionsError {
45    #[error(
46        "insufficient ESP balance: have {have} ESP, need {need} ESP to fund {delegators} \
47         delegators"
48    )]
49    InsufficientEsp {
50        have: String,
51        need: String,
52        delegators: usize,
53    },
54    #[error(
55        "insufficient ETH balance: have {have} ETH, need {need} ETH (including gas buffer) to \
56         fund {recipients} recipients"
57    )]
58    InsufficientEth {
59        have: String,
60        need: String,
61        recipients: usize,
62    },
63    #[error("delegation amount {amount} ESP is below minimum of {min} ESP")]
64    DelegationBelowMinimum { amount: String, min: String },
65    #[error(transparent)]
66    Transport(#[from] TransportError),
67    #[error(transparent)]
68    Contract(#[from] ContractError),
69    #[error(transparent)]
70    Commission(#[from] ParseCommissionError),
71    #[error(transparent)]
72    Other(#[from] anyhow::Error),
73}
74
75#[derive(Debug, Clone, Copy, Default, ValueEnum)]
76pub enum DelegationConfig {
77    EqualAmounts,
78    #[default]
79    VariableAmounts,
80    MultipleDelegators,
81    NoSelfDelegation,
82}
83
84// Manual implementation to match parsing of clap's ValueEnum
85impl fmt::Display for DelegationConfig {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let s = match self {
88            DelegationConfig::EqualAmounts => "equal-amounts",
89            DelegationConfig::VariableAmounts => "variable-amounts",
90            DelegationConfig::MultipleDelegators => "multiple-delegators",
91            DelegationConfig::NoSelfDelegation => "no-self-delegation",
92        };
93        write!(f, "{s}")
94    }
95}
96
97/// Validator registration info used by staking UI service tests.
98///
99/// Retrieved after calling `staking_cli::demo::create()` to get validator addresses.
100/// The staking UI service tests use these addresses to verify that registration
101/// events are correctly processed on the L1 stake table contract.
102#[derive(Clone, Debug)]
103pub struct RegistrationInfo {
104    pub from: Address,
105    pub commission: Commission,
106    pub payload: NodeSignatures,
107}
108
109/// Delegation info used by staking UI service tests.
110///
111/// Retrieved after calling `staking_cli::demo::create()` to get delegator addresses.
112/// The staking UI service tests use these addresses to verify that delegation
113/// events are correctly processed on the L1 stake table contract.
114#[derive(Clone, Debug)]
115pub struct DelegationInfo {
116    pub from: Address,
117    pub validator: Address,
118    pub amount: U256,
119}
120
121#[derive(Clone, Debug)]
122enum StakeTableTx {
123    SendEth {
124        to: Address,
125        amount: U256,
126    },
127    SendEsp {
128        to: Address,
129        amount: U256,
130    },
131    RegisterValidator {
132        from: Address,
133        commission: Commission,
134        payload: Box<NodeSignatures>,
135    },
136    Approve {
137        from: Address,
138        amount: U256,
139    },
140    Delegate {
141        from: Address,
142        validator: Address,
143        amount: U256,
144    },
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148enum SetupPhase {
149    Funding,
150    Approval,
151    Registration,
152    Delegation,
153}
154
155impl SetupPhase {
156    fn next(self) -> Option<Self> {
157        match self {
158            Self::Funding => Some(Self::Approval),
159            Self::Approval => Some(Self::Registration),
160            Self::Registration => Some(Self::Delegation),
161            Self::Delegation => None,
162        }
163    }
164}
165
166struct ValidatorConfig {
167    signer: PrivateKeySigner,
168    commission: Commission,
169    bls_key_pair: BLSKeyPair,
170    state_key_pair: StateKeyPair,
171    index: usize,
172}
173
174struct DelegatorConfig {
175    validator: Address,
176    signer: PrivateKeySigner,
177    delegate_amount: U256,
178}
179
180#[derive(Clone, Debug)]
181struct TransactionQueues {
182    funding: VecDeque<StakeTableTx>,
183    approvals: VecDeque<StakeTableTx>,
184    registration: VecDeque<StakeTableTx>,
185    delegations: VecDeque<StakeTableTx>,
186    current_phase: SetupPhase,
187}
188
189impl TransactionQueues {
190    fn current_group_mut(&mut self) -> &mut VecDeque<StakeTableTx> {
191        match self.current_phase {
192            SetupPhase::Funding => &mut self.funding,
193            SetupPhase::Approval => &mut self.approvals,
194            SetupPhase::Registration => &mut self.registration,
195            SetupPhase::Delegation => &mut self.delegations,
196        }
197    }
198
199    fn pop_next(&mut self) -> Option<StakeTableTx> {
200        loop {
201            if let Some(tx) = self.current_group_mut().pop_front() {
202                return Some(tx);
203            }
204            self.current_phase = self.current_phase.next()?;
205        }
206    }
207}
208
209#[derive(Clone, Debug)]
210struct TransactionProcessor<P> {
211    providers: HashMap<Address, P>,
212    funder: P,
213    stake_table: Address,
214    token: Address,
215}
216
217impl<P: Provider + Clone> TransactionProcessor<P> {
218    fn provider(&self, address: Address) -> Result<&P> {
219        self.providers
220            .get(&address)
221            .ok_or_else(|| anyhow::anyhow!("provider not found for {address}"))
222    }
223
224    async fn send_next(&self, tx: StakeTableTx) -> Result<PendingTransactionBuilder<Ethereum>> {
225        match tx {
226            StakeTableTx::SendEth { to, amount } => send_eth(&self.funder, to, amount).await,
227            StakeTableTx::SendEsp { to, amount } => {
228                send_esp(&self.funder, self.token, to, amount).await
229            },
230            StakeTableTx::RegisterValidator {
231                from,
232                commission,
233                payload,
234            } => {
235                let metadata_uri = "https://example.com/metadata".parse()?;
236                register_validator(
237                    self.provider(from)?,
238                    self.stake_table,
239                    commission,
240                    metadata_uri,
241                    *payload,
242                )
243                .await
244            },
245            StakeTableTx::Approve { from, amount } => {
246                approve(self.provider(from)?, self.token, self.stake_table, amount).await
247            },
248            StakeTableTx::Delegate {
249                from,
250                validator,
251                amount,
252            } => delegate(self.provider(from)?, self.stake_table, validator, amount).await,
253        }
254    }
255
256    async fn process_group(
257        &self,
258        txs: &mut VecDeque<StakeTableTx>,
259    ) -> Result<Vec<TransactionReceipt>> {
260        let mut pending = vec![];
261        while let Some(tx) = txs.pop_front() {
262            pending.push(self.send_next(tx).await?);
263        }
264        future::try_join_all(
265            pending
266                .into_iter()
267                .map(|p| async move { p.assert_success().await }),
268        )
269        .await
270    }
271}
272
273#[derive(Clone, Debug)]
274pub struct StakingTransactions<P> {
275    processor: TransactionProcessor<P>,
276    queues: TransactionQueues,
277}
278
279impl<P: Provider + Clone> StakingTransactions<P> {
280    /// Sends and awaits all transactions with high concurrency.
281    ///
282    /// This is the preferred way to make the changes to the stake table
283    /// contract as quickly as possible, while still allowing alloy's implicit
284    /// estimateGas calls to succeed.
285    ///
286    /// Ensures that dependent transactions are finalized before
287    /// continuing.
288    ///
289    /// The synchronization points are after
290    ///
291    /// 1. Ether + token funding
292    /// 2. Approvals
293    /// 3. Registrations
294    /// 4. Delegations
295    ///
296    /// For each them at least one L1 block will be required.
297    pub async fn apply_all(&mut self) -> Result<Vec<TransactionReceipt>> {
298        let mut receipts = Vec::new();
299
300        for queue in [
301            &mut self.queues.funding,
302            &mut self.queues.approvals,
303            &mut self.queues.registration,
304            &mut self.queues.delegations,
305        ] {
306            receipts.extend(self.processor.process_group(queue).await?);
307        }
308
309        tracing::info!("completed all staking transactions");
310
311        Ok(receipts)
312    }
313
314    /// Sends and awaits receipts on all funding and approval transactions
315    ///
316    /// If the caller wants more control but quickly get to a point where actual
317    /// changes are made to the stake table it is useful to call this function
318    /// first.
319    ///
320    /// This processes funding and approvals with a synchronization point between them.
321    pub async fn apply_prerequisites(&mut self) -> Result<Vec<TransactionReceipt>> {
322        if !matches!(self.queues.current_phase, SetupPhase::Funding) {
323            return Err(anyhow::anyhow!("apply_prerequisites must be called first"));
324        }
325
326        let mut receipts = Vec::new();
327
328        for queue in [&mut self.queues.funding, &mut self.queues.approvals] {
329            receipts.extend(self.processor.process_group(queue).await?);
330        }
331
332        self.queues.current_phase = SetupPhase::Registration;
333
334        Ok(receipts)
335    }
336
337    /// Sends and awaits one transaction
338    ///
339    /// The caller can use this function to rate limit changes to the L1 stake
340    /// table contract during setup.
341    pub async fn apply_one(&mut self) -> Result<Option<TransactionReceipt>> {
342        let Some(tx) = self.queues.pop_next() else {
343            return Ok(None);
344        };
345        let pending = self.processor.send_next(tx).await?;
346        Ok(Some(pending.assert_success().await?))
347    }
348
349    /// Returns pending validator registrations for staking UI service tests.
350    ///
351    /// Retrieves validator addresses that were set up by `staking_cli::demo::create()`.
352    /// Tests use these addresses to verify registration event processing.
353    pub fn registrations(&self) -> Vec<RegistrationInfo> {
354        self.queues
355            .registration
356            .iter()
357            .filter_map(|tx| {
358                if let StakeTableTx::RegisterValidator {
359                    from,
360                    commission,
361                    payload,
362                } = tx
363                {
364                    Some(RegistrationInfo {
365                        from: *from,
366                        commission: *commission,
367                        payload: *payload.clone(),
368                    })
369                } else {
370                    None
371                }
372            })
373            .collect()
374    }
375
376    /// Returns pending delegations for staking UI service tests.
377    ///
378    /// Used to retrieve delegator and validator addresses after calling `staking_cli::demo::create()`.
379    /// Tests use this data to verify delegation event processing in the staking UI service.
380    pub fn delegations(&self) -> Vec<DelegationInfo> {
381        self.queues
382            .delegations
383            .iter()
384            .filter_map(|tx| {
385                if let StakeTableTx::Delegate {
386                    from,
387                    validator,
388                    amount,
389                } = tx
390                {
391                    Some(DelegationInfo {
392                        from: *from,
393                        validator: *validator,
394                        amount: *amount,
395                    })
396                } else {
397                    None
398                }
399            })
400            .collect()
401    }
402
403    /// Returns the provider for a given address.
404    ///
405    /// This is useful when you need to get the provider for a delegator or validator
406    /// that was created during `StakingTransactions::create()`.
407    ///
408    /// Currently this is used by staking UI service to get the provider
409    /// so that we can also undelegate stake because we need the signer that
410    /// signed the delegate transaction
411    pub fn provider(&self, address: Address) -> Option<&P> {
412        self.processor.providers.get(&address)
413    }
414}
415
416impl StakingTransactions<HttpProviderWithWallet> {
417    /// Create staking transactions for test setup
418    ///
419    /// Prepares all transactions needed to setup the stake table with validators and delegations.
420    /// The transactions can be applied with different levels of concurrency using the methods on
421    /// the returned instance.
422    ///
423    /// Amounts used for funding, delegations, number of delegators are chosen somewhat arbitrarily.
424    ///
425    /// Assumptions:
426    ///
427    /// - Full control of Validators Ethereum wallets and the Ethereum node. Transactions are
428    ///   constructed in a way that they should always apply, if some (but not all) transactions
429    ///   fail to apply the easiest fix is probably to re-deploy the Ethereum network. Recovery,
430    ///   replacing of transactions is not implemented.
431    ///
432    /// - Nobody else is using the Ethereum accounts for anything else between calling this function
433    ///   and applying the returned transactions.
434    ///
435    /// Requirements:
436    ///
437    /// - token_holder: Requires Eth to fund validators and delegators, ESP tokens to fund delegators.
438    ///
439    /// Errors:
440    ///
441    /// - If Eth or ESP balances of the token_holder are insufficient.
442    /// - If any RPC request to the Ethereum node or contract calls fail.
443    pub async fn create(
444        rpc_url: Url,
445        token_holder: &(impl Provider + WalletProvider<Wallet = EthereumWallet>),
446        stake_table: Address,
447        validators: Vec<(PrivateKeySigner, BLSKeyPair, StateKeyPair)>,
448        num_delegators_per_validator: Option<u64>,
449        config: DelegationConfig,
450    ) -> Result<Self, CreateTransactionsError> {
451        tracing::info!(%stake_table, "staking to stake table contract for demo");
452
453        let token = fetch_token_address(rpc_url.clone(), stake_table).await?;
454
455        // Create shared HTTP client to reuse connection pool across all providers. Avoids creating
456        // too many connections to our geth node.
457        let shared_client = RpcClient::new(Http::new(rpc_url), /*is_local*/ true);
458
459        let token_holder_provider = ProviderBuilder::new()
460            .wallet(token_holder.wallet().clone())
461            .connect_client(shared_client.clone());
462
463        tracing::info!("ESP token address: {token}");
464        let token_holder_addr = token_holder.default_signer_address();
465        let token_balance = EspToken::new(token, &token_holder_provider)
466            .balanceOf(token_holder_addr)
467            .call()
468            .await?;
469        tracing::info!(
470            "token distributor account {} balance: {} ESP",
471            token_holder_addr,
472            format_ether(token_balance)
473        );
474
475        let fund_amount_esp = parse_ether("1000").unwrap();
476        let fund_amount_eth = parse_ether("10").unwrap();
477
478        let seed = [42u8; 32];
479        let mut rng = ChaCha20Rng::from_seed(seed);
480
481        let mut validator_info = vec![];
482        for (val_index, (signer, bls_key_pair, state_key_pair)) in
483            validators.into_iter().enumerate()
484        {
485            let commission = Commission::try_from(100u64 + 10u64 * val_index as u64)?;
486
487            validator_info.push(ValidatorConfig {
488                signer,
489                commission,
490                bls_key_pair,
491                state_key_pair,
492                index: val_index,
493            });
494        }
495
496        let mut delegator_info = vec![];
497
498        for validator in &validator_info {
499            let delegate_amount = match config {
500                DelegationConfig::EqualAmounts => Some(parse_ether("100").unwrap()),
501                DelegationConfig::MultipleDelegators | DelegationConfig::VariableAmounts => {
502                    Some(parse_ether("100").unwrap() * U256::from(validator.index % 5 + 1))
503                },
504                DelegationConfig::NoSelfDelegation => None,
505            };
506
507            if let Some(amount) = delegate_amount {
508                delegator_info.push(DelegatorConfig {
509                    validator: validator.signer.address(),
510                    signer: validator.signer.clone(),
511                    delegate_amount: amount,
512                });
513            }
514        }
515
516        if matches!(
517            config,
518            DelegationConfig::MultipleDelegators | DelegationConfig::NoSelfDelegation
519        ) {
520            for validator in &validator_info {
521                let delegators_per_validator = num_delegators_per_validator
522                    .map(|n| n as usize)
523                    .unwrap_or_else(|| rng.gen_range(2..=5));
524                for _ in 0..delegators_per_validator {
525                    let random_amount: u64 = rng.gen_range(100..=500);
526                    delegator_info.push(DelegatorConfig {
527                        validator: validator.signer.address(),
528                        signer: PrivateKeySigner::random(),
529                        delegate_amount: parse_ether(&random_amount.to_string()).unwrap(),
530                    });
531                }
532            }
533        }
534
535        let st = StakeTableV2::new(stake_table, &token_holder_provider);
536        let version: StakeTableContractVersion = st.getVersion().call().await?.try_into()?;
537        if let StakeTableContractVersion::V2 = version {
538            let min_delegate_amount = st.minDelegateAmount().call().await?;
539            for delegator in &delegator_info {
540                if delegator.delegate_amount < min_delegate_amount {
541                    return Err(CreateTransactionsError::DelegationBelowMinimum {
542                        amount: format_ether(delegator.delegate_amount),
543                        min: format_ether(min_delegate_amount),
544                    });
545                }
546            }
547        }
548
549        let mut funding = VecDeque::new();
550
551        let eth_recipients: HashSet<Address> = validator_info
552            .iter()
553            .map(|v| v.signer.address())
554            .chain(delegator_info.iter().map(|d| d.signer.address()))
555            .collect();
556
557        for &address in &eth_recipients {
558            funding.push_back(StakeTableTx::SendEth {
559                to: address,
560                amount: fund_amount_eth,
561            });
562        }
563
564        for delegator in &delegator_info {
565            funding.push_back(StakeTableTx::SendEsp {
566                to: delegator.signer.address(),
567                amount: fund_amount_esp,
568            });
569        }
570
571        // Only create one provider per address to avoid nonce errors.
572        let mut providers: HashMap<Address, _> = HashMap::new();
573
574        let mut registration = VecDeque::new();
575
576        for validator in &validator_info {
577            let address = validator.signer.address();
578            providers.entry(address).or_insert_with(|| {
579                ProviderBuilder::new()
580                    .wallet(EthereumWallet::from(validator.signer.clone()))
581                    .connect_client(shared_client.clone())
582            });
583
584            let payload =
585                NodeSignatures::create(address, &validator.bls_key_pair, &validator.state_key_pair);
586            registration.push_back(StakeTableTx::RegisterValidator {
587                from: address,
588                commission: validator.commission,
589                payload: Box::new(payload),
590            });
591        }
592
593        let mut approvals = VecDeque::new();
594        let mut delegations = VecDeque::new();
595
596        for delegator in &delegator_info {
597            let address = delegator.signer.address();
598            providers.entry(address).or_insert_with(|| {
599                ProviderBuilder::new()
600                    .wallet(EthereumWallet::from(delegator.signer.clone()))
601                    .connect_client(shared_client.clone())
602            });
603
604            approvals.push_back(StakeTableTx::Approve {
605                from: address,
606                amount: delegator.delegate_amount,
607            });
608
609            delegations.push_back(StakeTableTx::Delegate {
610                from: address,
611                validator: delegator.validator,
612                amount: delegator.delegate_amount,
613            });
614        }
615
616        let esp_required = fund_amount_esp * U256::from(delegator_info.len());
617        let eth_required = fund_amount_eth * U256::from(eth_recipients.len()) * U256::from(2);
618
619        if token_balance < esp_required {
620            return Err(CreateTransactionsError::InsufficientEsp {
621                have: format_ether(token_balance),
622                need: format_ether(esp_required),
623                delegators: delegator_info.len(),
624            });
625        }
626
627        let eth_balance = token_holder_provider.get_balance(token_holder_addr).await?;
628        if eth_balance < eth_required {
629            return Err(CreateTransactionsError::InsufficientEth {
630                have: format_ether(eth_balance),
631                need: format_ether(eth_required),
632                recipients: eth_recipients.len(),
633            });
634        }
635
636        Ok(StakingTransactions {
637            processor: TransactionProcessor {
638                providers,
639                funder: token_holder_provider,
640                stake_table,
641                token,
642            },
643            queues: TransactionQueues {
644                funding,
645                approvals,
646                registration,
647                delegations,
648                current_phase: SetupPhase::Funding,
649            },
650        })
651    }
652}
653
654/// Register validators, and delegate to themselves for demo purposes.
655///
656/// The environment variables used only for this function but not for the normal staking CLI are
657/// loaded directly from the environment.
658///
659/// Account indexes 20+ of the dev mnemonic are used for the validator accounts.
660pub async fn stake_for_demo(
661    config: &Config,
662    num_validators: u16,
663    num_delegators_per_validator: Option<u64>,
664    delegation_config: DelegationConfig,
665) -> Result<()> {
666    tracing::info!("staking to stake table contract for demo");
667
668    // let grant_recipient = mk_signer(config.signer.account_index.unwrap())?;
669    let grant_recipient = build_provider(
670        config.signer.mnemonic.clone().unwrap(),
671        config.signer.account_index.unwrap(),
672        config.rpc_url.clone(),
673        /* polling_interval */ None,
674    );
675
676    tracing::info!(
677        "grant recipient account for token funding: {}",
678        grant_recipient.default_signer_address()
679    );
680
681    let token_address =
682        fetch_token_address(config.rpc_url.clone(), config.stake_table_address).await?;
683    tracing::info!("ESP token address: {}", token_address);
684    let stake_table_address = config.stake_table_address;
685    tracing::info!("stake table address: {}", stake_table_address);
686
687    let mut validator_keys = vec![];
688    for val_index in 0..num_validators {
689        let signer = build_signer(
690            config.signer.mnemonic.clone().unwrap(),
691            20u32 + val_index as u32,
692        );
693
694        let consensus_private_key = parse_bls_priv_key(&dotenvy::var(format!(
695            "ESPRESSO_DEMO_SEQUENCER_STAKING_PRIVATE_KEY_{val_index}"
696        ))?)?
697        .into();
698        let state_private_key = parse_state_priv_key(&dotenvy::var(format!(
699            "ESPRESSO_DEMO_SEQUENCER_STATE_PRIVATE_KEY_{val_index}"
700        ))?)?;
701        validator_keys.push((
702            signer,
703            consensus_private_key,
704            StateKeyPair::from_sign_key(state_private_key),
705        ));
706    }
707
708    StakingTransactions::create(
709        config.rpc_url.clone(),
710        &grant_recipient,
711        config.stake_table_address,
712        validator_keys,
713        num_delegators_per_validator,
714        delegation_config,
715    )
716    .await?
717    .apply_all()
718    .await?;
719
720    Ok(())
721}
722
723#[cfg(test)]
724mod test {
725    use alloy::providers::ext::AnvilApi as _;
726    use espresso_types::v0_3::Validator;
727    use hotshot_types::signature_key::BLSPubKey;
728    use pretty_assertions::assert_matches;
729    use rand::rngs::StdRng;
730
731    use super::*;
732    use crate::{deploy::TestSystem, info::stake_table_info};
733
734    async fn shared_setup(
735        config: DelegationConfig,
736    ) -> Result<(Validator<BLSPubKey>, Validator<BLSPubKey>)> {
737        let system = TestSystem::deploy().await?;
738
739        let mut rng = StdRng::from_seed([42u8; 32]);
740        let keys = vec![
741            TestSystem::gen_keys(&mut rng),
742            TestSystem::gen_keys(&mut rng),
743        ];
744
745        StakingTransactions::create(
746            system.rpc_url.clone(),
747            &system.provider,
748            system.stake_table,
749            keys,
750            None,
751            config,
752        )
753        .await?
754        .apply_all()
755        .await?;
756        let l1_block_number = system.provider.get_block_number().await?;
757        let st = stake_table_info(system.rpc_url, system.stake_table, l1_block_number).await?;
758
759        // The stake table should have 2 validators
760        assert_eq!(st.len(), 2);
761        let val1 = st[0].clone();
762        let val2 = st[1].clone();
763
764        // The validators are not the same
765        assert_ne!(val1.account, val2.account);
766
767        Ok((val1, val2))
768    }
769
770    #[test_log::test(tokio::test)]
771    async fn test_stake_for_demo_equal_amounts() -> Result<()> {
772        let (val1, val2) = shared_setup(DelegationConfig::EqualAmounts).await?;
773
774        // The total stake of the validator is equal to it's own delegation
775        assert_eq!(val1.delegators.get(&val1.account), Some(&val1.stake));
776        assert_eq!(val2.delegators.get(&val2.account), Some(&val2.stake));
777
778        // The are no other delegators
779        assert_eq!(val1.delegators.len(), 1);
780        assert_eq!(val2.delegators.len(), 1);
781
782        // The stake amounts are equal
783        assert_eq!(val1.stake, val2.stake);
784
785        Ok(())
786    }
787
788    #[test_log::test(tokio::test)]
789    async fn test_stake_for_demo_variable_amounts() -> Result<()> {
790        let (val1, val2) = shared_setup(DelegationConfig::VariableAmounts).await?;
791
792        // The total stake of the validator is equal to it's own delegation
793        assert_eq!(val1.delegators.get(&val1.account), Some(&val1.stake));
794        assert_eq!(val2.delegators.get(&val2.account), Some(&val2.stake));
795
796        // The are no other delegators
797        assert_eq!(val1.delegators.len(), 1);
798        assert_eq!(val2.delegators.len(), 1);
799
800        // The stake amounts are not equal
801        assert_ne!(val1.stake, val2.stake);
802
803        Ok(())
804    }
805
806    #[test_log::test(tokio::test)]
807    async fn test_stake_for_demo_multiple_delegators() -> Result<()> {
808        let (val1, val2) = shared_setup(DelegationConfig::MultipleDelegators).await?;
809
810        // The total stake of the validator is not equal to it's own delegation
811        assert_ne!(val1.delegators.get(&val1.account), Some(&val1.stake));
812        assert_ne!(val2.delegators.get(&val2.account), Some(&val2.stake));
813
814        // The are other delegators
815        assert!(val1.delegators.len() > 1);
816        assert!(val2.delegators.len() > 1);
817
818        // The stake amounts are not equal
819        assert_ne!(val1.stake, val2.stake);
820
821        Ok(())
822    }
823
824    #[test_log::test(tokio::test)]
825    async fn test_stake_for_demo_no_self_delegation() -> Result<()> {
826        let (val1, val2) = shared_setup(DelegationConfig::NoSelfDelegation).await?;
827
828        // The validators have no self delegation
829        assert_eq!(val1.delegators.get(&val1.account), None);
830        assert_eq!(val2.delegators.get(&val2.account), None);
831
832        // The are other delegators
833        assert!(val1.delegators.len() > 1);
834        assert!(val2.delegators.len() > 1);
835
836        // The stake amounts are not equal
837        assert_ne!(val1.stake, val2.stake);
838
839        Ok(())
840    }
841
842    #[test_log::test(tokio::test)]
843    async fn test_configurable_delegators_per_validator() -> Result<()> {
844        let system = TestSystem::deploy().await?;
845
846        let mut rng = StdRng::from_seed([42u8; 32]);
847        let keys = vec![
848            TestSystem::gen_keys(&mut rng),
849            TestSystem::gen_keys(&mut rng),
850        ];
851
852        StakingTransactions::create(
853            system.rpc_url.clone(),
854            &system.provider,
855            system.stake_table,
856            keys,
857            Some(10),
858            DelegationConfig::MultipleDelegators,
859        )
860        .await?
861        .apply_all()
862        .await?;
863        let l1_block_number = system.provider.get_block_number().await?;
864        let st = stake_table_info(system.rpc_url, system.stake_table, l1_block_number).await?;
865
866        assert_eq!(st.len(), 2);
867
868        let val1 = &st[0];
869        let val2 = &st[1];
870
871        // Each validator should have exactly 10 additional delegators plus self-delegation
872        assert_eq!(val1.delegators.len(), 11);
873        assert_eq!(val2.delegators.len(), 11);
874
875        Ok(())
876    }
877
878    enum Failure {
879        Esp,
880        Eth,
881    }
882
883    #[rstest::rstest]
884    #[case::esp(Failure::Esp)]
885    #[case::eth(Failure::Eth)]
886    #[test_log::test(tokio::test)]
887    async fn test_insufficient_balance(#[case] case: Failure) -> Result<()> {
888        let system = TestSystem::deploy().await?;
889
890        let drain_address = PrivateKeySigner::random().address();
891
892        match case {
893            Failure::Esp => {
894                let balance = system
895                    .balance(system.provider.default_signer_address())
896                    .await?;
897                system.transfer(drain_address, balance).await?;
898            },
899            Failure::Eth => {
900                let eth_balance = system
901                    .provider
902                    .get_balance(system.provider.default_signer_address())
903                    .await?;
904                // keep a bit for estimateGas calls to succeed
905                let drain_amount = eth_balance - parse_ether("1").unwrap();
906                system.transfer_eth(drain_address, drain_amount).await?;
907            },
908        }
909
910        let mut rng = StdRng::from_seed([42u8; 32]);
911        let keys = vec![TestSystem::gen_keys(&mut rng)];
912
913        let result = StakingTransactions::create(
914            system.rpc_url.clone(),
915            &system.provider,
916            system.stake_table,
917            keys,
918            None,
919            DelegationConfig::EqualAmounts,
920        )
921        .await;
922
923        let err = result.expect_err("should fail with insufficient balance");
924        match case {
925            Failure::Esp => assert_matches!(err, CreateTransactionsError::InsufficientEsp { .. }),
926            Failure::Eth => assert_matches!(err, CreateTransactionsError::InsufficientEth { .. }),
927        };
928
929        Ok(())
930    }
931
932    #[test_log::test(tokio::test)]
933    async fn test_delegation_below_minimum() -> Result<()> {
934        let system = TestSystem::deploy().await?;
935
936        // Set min delegate amount higher than the demo amounts (100-500 ESP range)
937        let high_min = parse_ether("2000")?;
938        system.set_min_delegate_amount(high_min).await?;
939
940        let mut rng = StdRng::from_seed([42u8; 32]);
941        let keys = vec![TestSystem::gen_keys(&mut rng)];
942
943        // create() only prepares transactions, it doesn't send any, so returning
944        // an error here guarantees no transactions were broadcast
945        let result = StakingTransactions::create(
946            system.rpc_url.clone(),
947            &system.provider,
948            system.stake_table,
949            keys,
950            None,
951            DelegationConfig::EqualAmounts,
952        )
953        .await;
954
955        assert_matches!(
956            result,
957            Err(CreateTransactionsError::DelegationBelowMinimum { .. })
958        );
959
960        Ok(())
961    }
962
963    #[rstest::rstest]
964    #[case::equal_amounts(DelegationConfig::EqualAmounts)]
965    #[case::variable_amounts(DelegationConfig::VariableAmounts)]
966    #[case::multiple_delegators(DelegationConfig::MultipleDelegators)]
967    #[case::no_self_delegation(DelegationConfig::NoSelfDelegation)]
968    #[test_log::test(tokio::test)]
969    async fn test_setup_with_slow_blocks(#[case] config: DelegationConfig) -> Result<()> {
970        let system = TestSystem::deploy().await?;
971        system.provider.anvil_set_auto_mine(false).await?;
972        system.provider.anvil_set_interval_mining(1).await?;
973
974        let mut rng = StdRng::from_seed([42u8; 32]);
975        let keys = vec![
976            TestSystem::gen_keys(&mut rng),
977            TestSystem::gen_keys(&mut rng),
978        ];
979
980        StakingTransactions::create(
981            system.rpc_url.clone(),
982            &system.provider,
983            system.stake_table,
984            keys,
985            None,
986            config,
987        )
988        .await?
989        .apply_all()
990        .await?;
991        let l1_block_number = system.provider.get_block_number().await?;
992        let st = stake_table_info(system.rpc_url, system.stake_table, l1_block_number).await?;
993
994        assert_eq!(st.len(), 2);
995        assert!(st[0].stake > U256::ZERO);
996        assert!(st[1].stake > U256::ZERO);
997
998        if let DelegationConfig::NoSelfDelegation = config {
999            assert!(!st[0].delegators.contains_key(&st[0].account));
1000            assert!(!st[1].delegators.contains_key(&st[1].account));
1001        }
1002
1003        Ok(())
1004    }
1005}