From 3ed2e53b771b9d356e488447b033e7213d216b9d Mon Sep 17 00:00:00 2001 From: Francesco Medas <104889824+frankmeds@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:10:07 +0400 Subject: [PATCH] feat: DEVOPS-1808 Add z2 commands for depositTopup, unstake and withdraw (#2341) * feat: DEVOPS-1808 Add z2 commands for depositTopup, unstake and withdraw #2056 * fix command descriptions --- z2/Cargo.toml | 2 +- z2/docs/staking.md | 14 +- z2/src/bin/z2.rs | 108 ++++++++++-- z2/src/deployer.rs | 285 +++++++++++++++++++++++++++++- z2/src/plumbing.rs | 32 ++++ z2/src/utils.rs | 23 +++ z2/src/validators.rs | 400 +++++++++++++++++++++++++------------------ 7 files changed, 674 insertions(+), 190 deletions(-) diff --git a/z2/Cargo.toml b/z2/Cargo.toml index 952ebed96..046ed8c95 100644 --- a/z2/Cargo.toml +++ b/z2/Cargo.toml @@ -58,6 +58,7 @@ reqwest = {version = "0.12.9", features = ["json", "rustls-tls", "http2", "chars revm = {version = "19.4.0", features = ["optional_balance_check"]} rs-leveldb = "0.1.5" rustls = "0.23.22" +scopeguard = "1.2.0" serde = {version = "1.0.217", features = ["derive"]} serde_json = { version = "1.0.138", features = ["preserve_order"] } serde_yaml = "0.9.34" @@ -77,7 +78,6 @@ url = "2.5.4" zilliqa = {path = "../zilliqa"} zilliqa-rs = "0.3.2" zqutils = {git = "https://github.com/zilliqa/zq-base"} -scopeguard = "1.2.0" [build-dependencies] prost-build = "0.13.3" diff --git a/z2/docs/staking.md b/z2/docs/staking.md index 729c779a7..df8a04aa0 100644 --- a/z2/docs/staking.md +++ b/z2/docs/staking.md @@ -15,7 +15,7 @@ z2 deposit \ --signing-address -Usage: z2 deposit --chain --private-key --public-key --peer-id --deposit-auth-signature --amount --reward-address --signing-address +Usage: z2 deposit --chain --private-key --public-key --peer-id --deposit-auth-signature --amount --reward-address --signing-address ``` ## Parameters * `--chain `: The name of the chain. Possible values are zq2-devnet, zq2-prototestnet, zq2-protomainnet, zq2-testnet, zq2-mainnet. @@ -23,7 +23,7 @@ Usage: z2 deposit --chain --private-key `: The BLS public key of the validator node. * `--peer-id `: The peer ID of the validator node. * `--deposit-auth-signature `: BLS signature of the validator node signing over control address and chain Id. -* `--amount `: The amount in ZIL to deposit. The valid range is from 10 million to 255 million ZIL, allowing a deposit of up to 255 million ZIL. +* `--amount `: The amount in millions of ZIL to deposit. The valid range is from 10 million to 255 million ZIL, allowing a deposit of up to 255 million ZIL. * `--reward-address `: Specifies the address to receive rewards. You can generate a new wallet address to receive the rewards. * `--signing-address `: Specifies the address which signs cross-chain events. @@ -68,7 +68,7 @@ $ echo '{"secret_key":"96252e38af375be21d9eb30a6b88abc3836acecaeb2240731fb42e029 --private-key 96252e38af375be21d9eb30a6b88abc3836acecaeb2240731fb42e0299e14419 \ --reward-address 0xe29a3e99a6997B1571DA24d6517e7b3acaFB5d9e \ --signing-address 0x3946f9872247af2eb4fe44c81c463e801925b8d4 \ - --amount 100 \ + --amount 10 \ --public-key 825124961d51c99816848875fa505b75f2e62e69937fe9bfa5fa97711845abd667f05bdc3756f7dba6b7e9e0467a3804 \ --deposit-auth-signature b4770471f1b6b798b3a5cf19b6f574724777f2fbf7b7f520e75fc8461cafcfd84114316fe2aeaf35b52b9ca519310f8c0bf5cd941426e4a78cc7e10c6da80f245a9ddadc42de3f8a35db42d633b2b03847b33883f702eb13c332988d34d68d90 ``` @@ -84,14 +84,14 @@ z2 deposit-top-up \ --public-key --amount -Usage: z2 deposit-top-up --chain --private-key --public-key --amount +Usage: z2 deposit-top-up --chain --private-key --public-key --amount ``` ## Parameters * `--chain `: The name of the chain. Possible values are zq2-devnet, zq2-prototestnet, zq2-protomainnet, zq2-testnet, zq2-mainnet. * `--private-key `: The private key of the wallet. * `--public-key `: The BLS public key of the validator node. -* `--amount `: The amount in ZIL to top up. +* `--amount `: The amount in millions of ZILs to top up. #### Sample run ```bash @@ -112,14 +112,14 @@ z2 unstake \ --public-key --amount -Usage: z2 deposit-top-up --chain --private-key --public-key --amount +Usage: z2 deposit-top-up --chain --private-key --public-key --amount ``` ## Parameters * `--chain `: The name of the chain. Possible values are zq2-devnet, zq2-prototestnet, zq2-protomainnet, zq2-testnet, zq2-mainnet. * `--private-key `: The private key of the wallet. * `--public-key `: The BLS public key of the validator node. -* `--amount `: The amount in ZIL to unstake. +* `--amount `: The amount in millions of ZILs to unstake. #### Sample run ```bash diff --git a/z2/src/bin/z2.rs b/z2/src/bin/z2.rs index 1410abd22..f52f60295 100644 --- a/z2/src/bin/z2.rs +++ b/z2/src/bin/z2.rs @@ -94,8 +94,16 @@ enum DeployerCommands { GetConfigFile(DeployerConfigArgs), /// Generate in output the commands to deposit stake amount to all the validators GetDepositCommands(DeployerActionsArgs), - /// Deposit the stake amounts to all the validators + /// Deposit stake amounts to the internal validators Deposit(DeployerActionsArgs), + /// Top up stake to the internal validators + DepositTopUp(DeployerStakingArgs), + /// Unstake funds of the internal validators + Unstake(DeployerStakingArgs), + /// Withdraw unstaked funds to the internal validators + Withdraw(DeployerActionsArgs), + /// Show network stake information + Stakers(DeployerStakersArgs), /// Run RPC calls over the internal network nodes Rpc(DeployerRpcArgs), /// Run command over SSH in the internal network nodes @@ -184,6 +192,24 @@ pub struct DeployerActionsArgs { select: bool, } +#[derive(Args, Debug)] +pub struct DeployerStakingArgs { + /// The network deployer config file + config_file: Option, + /// Enable nodes selection + #[clap(long)] + select: bool, + /// Specify the amount in millions + #[clap(long)] + amount: u8, +} + +#[derive(Args, Debug)] +pub struct DeployerStakersArgs { + /// The network deployer config file + config_file: Option, +} + #[derive(Args, Debug)] pub struct DeployerMonitorArgs { /// The metric to display. Default: block-number @@ -506,7 +532,7 @@ struct DepositStruct { /// Specify the Validator deposit signature #[clap(long)] deposit_auth_signature: String, - /// Specify the stake amount you want provide + /// Specify the stake amount in millions you want provide #[clap(long, short)] amount: u8, /// Specify the staking reward address @@ -528,7 +554,7 @@ struct DepositTopUpStruct { /// Specify the Validator Public Key #[clap(long)] public_key: String, - /// Specify the stake amount you want provide + /// Specify the stake amount in millions you want provide #[clap(long, short)] amount: u8, } @@ -538,13 +564,13 @@ struct UnstakeStruct { /// Specify the ZQ2 deposit chain #[clap(long = "chain")] chain_name: chain::Chain, - /// Specify the private_key to fund the deposit + /// Specify the private_key that submitted the deposit transaction #[clap(long, short)] private_key: String, /// Specify the Validator Public Key #[clap(long)] public_key: String, - /// Specify the stake amount you want provide + /// Specify the amount in millions you want to unstake #[clap(long, short)] amount: u8, } @@ -554,7 +580,7 @@ struct WithdrawStruct { /// Specify the ZQ2 deposit chain #[clap(long = "chain")] chain_name: chain::Chain, - /// Specify the private_key to fund the deposit + /// Specify the private_key that submitted the deposit transaction #[clap(long, short)] private_key: String, /// Specify the Validator Public Key @@ -912,6 +938,19 @@ async fn main() -> Result<()> { })?; Ok(()) } + DeployerCommands::Stakers(ref arg) => { + let config_file = arg.config_file.clone().ok_or_else(|| { + anyhow::anyhow!( + "Provide a configuration file. [--config-file] mandatory argument" + ) + })?; + plumbing::run_deployer_stakers(&config_file) + .await + .map_err(|err| { + anyhow::anyhow!("Failed to run deployer stakers command: {}", err) + })?; + Ok(()) + } DeployerCommands::Deposit(ref arg) => { let config_file = arg.config_file.clone().ok_or_else(|| { anyhow::anyhow!( @@ -925,6 +964,45 @@ async fn main() -> Result<()> { })?; Ok(()) } + DeployerCommands::DepositTopUp(ref arg) => { + let config_file = arg.config_file.clone().ok_or_else(|| { + anyhow::anyhow!( + "Provide a configuration file. [--config-file] mandatory argument" + ) + })?; + plumbing::run_deployer_deposit_top_up(&config_file, arg.select, arg.amount) + .await + .map_err(|err| { + anyhow::anyhow!("Failed to run deployer deposit-top-up command: {}", err) + })?; + Ok(()) + } + DeployerCommands::Unstake(ref arg) => { + let config_file = arg.config_file.clone().ok_or_else(|| { + anyhow::anyhow!( + "Provide a configuration file. [--config-file] mandatory argument" + ) + })?; + plumbing::run_deployer_unstake(&config_file, arg.select, arg.amount) + .await + .map_err(|err| { + anyhow::anyhow!("Failed to run deployer unstake command: {}", err) + })?; + Ok(()) + } + DeployerCommands::Withdraw(ref arg) => { + let config_file = arg.config_file.clone().ok_or_else(|| { + anyhow::anyhow!( + "Provide a configuration file. [--config-file] mandatory argument" + ) + })?; + plumbing::run_deployer_withdraw(&config_file, arg.select) + .await + .map_err(|err| { + anyhow::anyhow!("Failed to run deployer withdraw command: {}", err) + })?; + Ok(()) + } DeployerCommands::Rpc(ref args) => { plumbing::run_deployer_rpc( &args.method, @@ -1137,7 +1215,7 @@ async fn main() -> Result<()> { .unwrap(), BlsSignature::from_string(&args.deposit_auth_signature).unwrap(), )?; - let client_config = validators::ClientConfig::new( + let signer_client = validators::SignerClient::new( &args.chain_name.get_api_endpoint()?, &args.private_key, )?; @@ -1146,37 +1224,39 @@ async fn main() -> Result<()> { &args.reward_address, &args.signing_address, )?; - validators::deposit(&node, &client_config, &deposit_params).await + signer_client.deposit(&node, &deposit_params).await } Commands::DepositTopUp(ref args) => { let bls_public_key = NodePublicKey::from_bytes(hex::decode(&args.public_key).unwrap().as_slice()) .unwrap(); - let client_config = validators::ClientConfig::new( + let signer_client = validators::SignerClient::new( &args.chain_name.get_api_endpoint()?, &args.private_key, )?; - validators::deposit_top_up(&client_config, &bls_public_key, args.amount).await + signer_client + .deposit_top_up(&bls_public_key, args.amount) + .await } Commands::Unstake(ref args) => { let bls_public_key = NodePublicKey::from_bytes(hex::decode(&args.public_key).unwrap().as_slice()) .unwrap(); - let client_config = validators::ClientConfig::new( + let signer_client = validators::SignerClient::new( &args.chain_name.get_api_endpoint()?, &args.private_key, )?; - validators::unstake(&client_config, &bls_public_key, args.amount).await + signer_client.unstake(&bls_public_key, args.amount).await } Commands::Withdraw(ref args) => { let bls_public_key = NodePublicKey::from_bytes(hex::decode(&args.public_key).unwrap().as_slice()) .unwrap(); - let client_config = validators::ClientConfig::new( + let signer_client = validators::SignerClient::new( &args.chain_name.get_api_endpoint()?, &args.private_key, )?; - validators::withdraw(&client_config, &bls_public_key, args.count).await + signer_client.withdraw(&bls_public_key, args.count).await } Commands::Nodes(ref args) => { let spec = Composition::parse(&args.nodes)?; diff --git a/z2/src/deployer.rs b/z2/src/deployer.rs index 76604abfd..353636503 100644 --- a/z2/src/deployer.rs +++ b/z2/src/deployer.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; use anyhow::{anyhow, Result}; use clap::ValueEnum; @@ -16,6 +19,7 @@ use crate::{ node::{ChainNode, NodePort, NodeRole}, }, secret::Secret, + utils::format_amount, validators, }; @@ -254,6 +258,69 @@ pub async fn get_node_deposit_commands(genesis_private_key: &str, node: &ChainNo Ok(()) } +pub async fn run_stakers(config_file: &str) -> Result<()> { + let config = NetworkConfig::from_file(config_file).await?; + let chain = ChainInstance::new(config.clone()).await?; + let mut validators = chain.nodes().await?; + validators.retain(|node| node.role == NodeRole::Bootstrap || node.role == NodeRole::Validator); + + println!("Retrieving the stakers info in the chain {}", chain.name()); + + let genesis_private_key = chain.genesis_private_key().await?; + let signer_client = + validators::SignerClient::new(&chain.chain()?.get_api_endpoint()?, &genesis_private_key)?; + println!("Loading the stakers..."); + let stakers = signer_client.get_stakers().await?; + + println!("Loading the internal nodes..."); + let mut internal_validators = HashMap::::new(); + for node in validators { + let private_keys = node.get_private_key().await?; + let node_ethereum_address = EthereumAddress::from_private_key(&private_keys)?; + + internal_validators.insert( + node_ethereum_address.bls_public_key.to_string(), + node.name(), + ); + + if !stakers.contains(&node_ethereum_address.bls_public_key) + && node.role == NodeRole::Validator + { + log::warn!("{} is NOT a validator", node.name()); + } + } + + for public_key in stakers { + let stake = signer_client.get_stake(&public_key).await? as f64 / 10f64.powi(18); + let future_stake = + signer_client.get_future_stake(&public_key).await? as f64 / 10f64.powi(18); + let public_key = &public_key.to_string(); + let name = internal_validators.get(public_key).unwrap_or(public_key); + + println!("---\n{}:", name.bold()); + + println!( + "\tStake {:>width$} $ZIL", + if stake != future_stake { + format_amount(stake).red() + } else { + format_amount(stake).normal() + }, + width = 30 + ); + + if stake != future_stake { + println!( + "\tFuture stake {:>width$} $ZIL", + format_amount(future_stake).green(), + width = 30 + ); + } + } + + Ok(()) +} + pub async fn run_deposit(config_file: &str, node_selection: bool) -> Result<()> { let config = NetworkConfig::from_file(config_file).await?; let chain = ChainInstance::new(config.clone()).await?; @@ -296,14 +363,14 @@ pub async fn run_deposit(config_file: &str, node_selection: bool) -> Result<()> SecretKey::from_hex(&genesis_private_key)?.to_evm_address(), ); - println!("Validator {}:", node.name()); + println!("{}", format!("Validator {}:", node.name()).yellow()); let validator = validators::Validator::new( node_ethereum_address.peer_id, node_ethereum_address.bls_public_key, deposit_auth_signature, )?; - let client_config = validators::ClientConfig::new( + let signer_client = validators::SignerClient::new( &chain.chain()?.get_api_endpoint()?, &genesis_private_key, )?; @@ -313,7 +380,7 @@ pub async fn run_deposit(config_file: &str, node_selection: bool) -> Result<()> ZERO_ACCOUNT, )?; - let result = validators::deposit(&validator, &client_config, &deposit_params).await; + let result = signer_client.deposit(&validator, &deposit_params).await; match result { Ok(()) => successes.push(node.name()), @@ -338,6 +405,216 @@ pub async fn run_deposit(config_file: &str, node_selection: bool) -> Result<()> Ok(()) } +pub async fn run_deposit_top_up(config_file: &str, node_selection: bool, amount: u8) -> Result<()> { + let config = NetworkConfig::from_file(config_file).await?; + let chain = ChainInstance::new(config.clone()).await?; + let mut validators = chain.nodes().await?; + validators.retain(|node| node.role == NodeRole::Validator); + + let validator_names = validators + .iter() + .map(|n| n.name().clone()) + .collect::>(); + + let selected_machines = if !node_selection { + validator_names + } else { + let mut multi_select = cliclack::multiselect("Select nodes"); + + for name in validator_names { + multi_select = multi_select.item(name.clone(), name, ""); + } + + multi_select.interact()? + }; + + validators.retain(|node| selected_machines.clone().contains(&node.name())); + + println!( + "Running stake deposit top-up for the validators in the chain {}", + chain.name() + ); + + let mut successes = vec![]; + let mut failures = vec![]; + + for node in validators { + let genesis_private_key = chain.genesis_private_key().await?; + let private_keys = node.get_private_key().await?; + let node_ethereum_address = EthereumAddress::from_private_key(&private_keys)?; + + let signer_client = validators::SignerClient::new( + &chain.chain()?.get_api_endpoint()?, + &genesis_private_key, + )?; + + println!("{}", format!("Validator {}:", node.name()).yellow()); + let result = signer_client + .deposit_top_up(&node_ethereum_address.bls_public_key, amount) + .await; + + match result { + Ok(()) => successes.push(node.name()), + Err(err) => { + println!("Node {} failed with error: {}", node.name(), err); + failures.push(node.name()); + } + } + } + + for success in successes { + log::info!("SUCCESS: {}", success); + } + + if !failures.is_empty() { + for failure in failures { + log::error!("FAILURE: {}", failure); + } + } + + Ok(()) +} + +pub async fn run_unstake(config_file: &str, node_selection: bool, amount: u8) -> Result<()> { + let config = NetworkConfig::from_file(config_file).await?; + let chain = ChainInstance::new(config.clone()).await?; + let mut validators = chain.nodes().await?; + validators.retain(|node| node.role == NodeRole::Validator); + + let validator_names = validators + .iter() + .map(|n| n.name().clone()) + .collect::>(); + + let selected_machines = if !node_selection { + validator_names + } else { + let mut multi_select = cliclack::multiselect("Select nodes"); + + for name in validator_names { + multi_select = multi_select.item(name.clone(), name, ""); + } + + multi_select.interact()? + }; + + validators.retain(|node| selected_machines.clone().contains(&node.name())); + + println!( + "Running unstake for the validators in the chain {}", + chain.name() + ); + + let mut successes = vec![]; + let mut failures = vec![]; + + for node in validators { + let genesis_private_key = chain.genesis_private_key().await?; + let private_keys = node.get_private_key().await?; + let node_ethereum_address = EthereumAddress::from_private_key(&private_keys)?; + + let signer_client = validators::SignerClient::new( + &chain.chain()?.get_api_endpoint()?, + &genesis_private_key, + )?; + + println!("{}", format!("Validator {}:", node.name()).yellow()); + let result = signer_client + .unstake(&node_ethereum_address.bls_public_key, amount) + .await; + + match result { + Ok(()) => successes.push(node.name()), + Err(err) => { + println!("Node {} failed with error: {}", node.name(), err); + failures.push(node.name()); + } + } + } + + for success in successes { + log::info!("SUCCESS: {}", success); + } + + if !failures.is_empty() { + for failure in failures { + log::error!("FAILURE: {}", failure); + } + } + + Ok(()) +} + +pub async fn run_withdraw(config_file: &str, node_selection: bool) -> Result<()> { + let config = NetworkConfig::from_file(config_file).await?; + let chain = ChainInstance::new(config.clone()).await?; + let mut validators = chain.nodes().await?; + validators.retain(|node| node.role == NodeRole::Validator); + + let validator_names = validators + .iter() + .map(|n| n.name().clone()) + .collect::>(); + + let selected_machines = if !node_selection { + validator_names + } else { + let mut multi_select = cliclack::multiselect("Select nodes"); + + for name in validator_names { + multi_select = multi_select.item(name.clone(), name, ""); + } + + multi_select.interact()? + }; + + validators.retain(|node| selected_machines.clone().contains(&node.name())); + + println!( + "Running withdraw for the validators in the chain {}", + chain.name() + ); + + let mut successes = vec![]; + let mut failures = vec![]; + + for node in validators { + let genesis_private_key = chain.genesis_private_key().await?; + let private_keys = node.get_private_key().await?; + let node_ethereum_address = EthereumAddress::from_private_key(&private_keys)?; + + let signer_client = validators::SignerClient::new( + &chain.chain()?.get_api_endpoint()?, + &genesis_private_key, + )?; + + println!("{}", format!("Validator {}:", node.name()).yellow()); + let result = signer_client + .withdraw(&node_ethereum_address.bls_public_key, 0) + .await; + + match result { + Ok(()) => successes.push(node.name()), + Err(err) => { + println!("Node {} failed with error: {}", node.name(), err); + failures.push(node.name()); + } + } + } + + for success in successes { + log::info!("SUCCESS: {}", success); + } + + if !failures.is_empty() { + for failure in failures { + log::error!("FAILURE: {}", failure); + } + } + + Ok(()) +} + pub async fn run_rpc_call( method: &str, params: &Option, diff --git a/z2/src/plumbing.rs b/z2/src/plumbing.rs index b8343e39a..6b22529da 100644 --- a/z2/src/plumbing.rs +++ b/z2/src/plumbing.rs @@ -229,12 +229,44 @@ pub async fn run_deployer_get_deposit_commands( Ok(()) } +pub async fn run_deployer_stakers(config_file: &str) -> Result<()> { + println!("🦆 Running stakers data for {config_file} .. "); + deployer::run_stakers(config_file).await?; + Ok(()) +} + pub async fn run_deployer_deposit(config_file: &str, node_selection: bool) -> Result<()> { println!("🦆 Running deposit for {config_file} .. "); deployer::run_deposit(config_file, node_selection).await?; Ok(()) } +pub async fn run_deployer_deposit_top_up( + config_file: &str, + node_selection: bool, + amount: u8, +) -> Result<()> { + println!("🦆 Running deposit-top-up for {config_file} .. "); + deployer::run_deposit_top_up(config_file, node_selection, amount).await?; + Ok(()) +} + +pub async fn run_deployer_unstake( + config_file: &str, + node_selection: bool, + amount: u8, +) -> Result<()> { + println!("🦆 Running unstake for {config_file} .. "); + deployer::run_unstake(config_file, node_selection, amount).await?; + Ok(()) +} + +pub async fn run_deployer_withdraw(config_file: &str, node_selection: bool) -> Result<()> { + println!("🦆 Running withdraw for {config_file} .. "); + deployer::run_withdraw(config_file, node_selection).await?; + Ok(()) +} + pub async fn run_deployer_rpc( method: &str, params: &Option, diff --git a/z2/src/utils.rs b/z2/src/utils.rs index 50b1a9b68..356f77682 100644 --- a/z2/src/utils.rs +++ b/z2/src/utils.rs @@ -129,3 +129,26 @@ pub fn string_decimal_to_hex(input: &str) -> Result { Err(anyhow!("Invalid decimal number provided.")) } + +pub fn format_amount(number: f64) -> String { + // Separate the integer and fractional parts + let integer_part = number.trunc() as u64; // Get the integer part + let fractional_part = number.fract(); // Get the fractional part + + // Format the integer part with commas + let mut integer_str = integer_part.to_string(); + let mut formatted_integer = String::new(); + while integer_str.len() > 3 { + let len = integer_str.len(); + formatted_integer = format!(",{}{}", &integer_str[len - 3..], formatted_integer); + integer_str.truncate(len - 3); + } + formatted_integer = format!("{}{}", integer_str, formatted_integer); + + // Format the fractional part with six decimal places + let formatted_fractional = format!("{:.18}", fractional_part); + let formatted_fractional = formatted_fractional.trim_start_matches('0'); + + // Combine the integer and fractional parts + format!("{}{}", formatted_integer, formatted_fractional) +} diff --git a/z2/src/validators.rs b/z2/src/validators.rs index 856c34686..4413ea6ff 100644 --- a/z2/src/validators.rs +++ b/z2/src/validators.rs @@ -1,10 +1,11 @@ /// Code to render the validator join configuration and startup script. use std::env; -use std::{convert::TryFrom, path::Path}; +use std::{convert::TryFrom, path::Path, sync::Arc}; use anyhow::{anyhow, Context as _, Result}; use ethabi::Token; use ethers::{ + contract::abigen, core::types::TransactionRequest, middleware::SignerMiddleware, providers::{Http, Middleware, Provider}, @@ -46,18 +47,250 @@ impl Validator { } #[derive(Debug)] -pub struct ClientConfig { +pub struct SignerClient { chain_endpoint: String, private_key: String, } -impl ClientConfig { +impl SignerClient { pub fn new(chain_endpoint: &str, private_key: &str) -> Result { Ok(Self { chain_endpoint: chain_endpoint.to_owned(), private_key: private_key.to_owned(), }) } + + async fn get_signer(&self) -> Result, LocalWallet>> { + let provider = Provider::::try_from(self.chain_endpoint.clone())?; + + let wallet: LocalWallet = self + .private_key + .as_str() + .parse::()? + .with_chain_id(provider.get_chainid().await?.as_u64()); + + Ok(SignerMiddleware::new(provider, wallet)) + } + + pub async fn deposit(&self, validator: &Validator, params: &DepositParams) -> Result<()> { + println!( + "Deposit: adding {} M $ZIL to {}", + params.amount, validator.peer_id + ); + + let client = self.get_signer().await?; + + // Stake the new validator's funds. + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .value(params.amount as u128 * 1_000_000u128 * 10u128.pow(18)) + .data( + contracts::deposit_v4::DEPOSIT + .encode_input(&[ + Token::Bytes(validator.public_key.as_bytes()), + Token::Bytes(validator.peer_id.to_bytes()), + Token::Bytes(validator.deposit_auth_signature.to_bytes()), + Token::Address(params.reward_address), + Token::Address(params.signing_address), + ]) + .unwrap(), + ); + + // send it! + let pending_tx = client.send_transaction(tx, None).await?; + + // get the mined tx + let receipt = pending_tx + .await? + .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; + let tx = client.get_transaction(receipt.transaction_hash).await?; + + println!("Sent tx: {}\n", serde_json::to_string(&tx)?); + println!("Tx receipt: {}", serde_json::to_string(&receipt)?); + + Ok(()) + } + + pub async fn deposit_top_up(&self, bls_public_key: &NodePublicKey, amount: u8) -> Result<()> { + println!("DepositTopUp: adding {} M $ZIL stake", amount,); + + let client = self.get_signer().await?; + + // Topup the validator's funds. + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .value(amount as u128 * 1_000_000u128 * 10u128.pow(18)) + .data( + contracts::deposit_v4::DEPOSIT_TOPUP + .encode_input(&[Token::Bytes(bls_public_key.as_bytes())]) + .unwrap(), + ); + + // send it! + let pending_tx = client.send_transaction(tx, None).await?; + + // get the mined tx + let receipt = pending_tx + .await? + .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; + let tx = client.get_transaction(receipt.transaction_hash).await?; + + println!("Sent tx: {}\n", serde_json::to_string(&tx)?); + println!("Tx receipt: {}", serde_json::to_string(&receipt)?); + + Ok(()) + } + + pub async fn unstake(&self, bls_public_key: &NodePublicKey, amount: u8) -> Result<()> { + println!("Unstake: removing {} M $ZIL", amount); + + let client = self.get_signer().await?; + // Unstake the validator's funds. + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .data( + contracts::deposit_v4::UNSTAKE + .encode_input(&[ + Token::Bytes(bls_public_key.as_bytes()), + Token::Uint((amount as u128 * 1_000_000u128 * 10u128.pow(18)).into()), + ]) + .unwrap(), + ); + + // send it! + let pending_tx = client.send_transaction(tx, None).await?; + + // get the mined tx + let receipt = pending_tx + .await? + .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; + let tx = client.get_transaction(receipt.transaction_hash).await?; + + println!("Sent tx: {}\n", serde_json::to_string(&tx)?); + println!("Tx receipt: {}", serde_json::to_string(&receipt)?); + + Ok(()) + } + + pub async fn withdraw(&self, bls_public_key: &NodePublicKey, count: u8) -> Result<()> { + println!("Withdraw: pulling available unstaked funds from deposit contract"); + + let client = self.get_signer().await?; + // Withdraw the validator's funds. + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .data( + contracts::deposit_v4::UNSTAKE + .encode_input(&[ + Token::Bytes(bls_public_key.as_bytes()), + Token::Uint((count as u128).into()), + ]) + .unwrap(), + ); + + // send it! + let pending_tx = client.send_transaction(tx, None).await?; + + // get the mined tx + let receipt = pending_tx + .await? + .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; + let tx = client.get_transaction(receipt.transaction_hash).await?; + + println!("Sent tx: {}\n", serde_json::to_string(&tx)?); + println!("Tx receipt: {}", serde_json::to_string(&receipt)?); + + Ok(()) + } + + pub async fn get_stake(&self, public_key: &NodePublicKey) -> Result { + let client = self.get_signer().await?; + + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .data( + contracts::deposit_v4::GET_STAKE + .encode_input(&[Token::Bytes(public_key.as_bytes())]) + .unwrap(), + ); + let output = client.call(&tx.into(), None).await.unwrap(); + + Ok(contracts::deposit_v4::GET_STAKE + .decode_output(&output) + .unwrap()[0] + .clone() + .into_uint() + .unwrap() + .as_u128()) + } + + pub async fn get_future_stake(&self, public_key: &NodePublicKey) -> Result { + let client = self.get_signer().await?; + + abigen!( + DEPOSIT_V4, + r#"[ + function getFutureStake(bytes calldata blsPubKey) public view returns (uint256) + ]"#, + derives(serde::Deserialize, serde::Serialize); + ); + + let client = Arc::new(client.provider().to_owned()); + let contract = DEPOSIT_V4::new(H160(contract_addr::DEPOSIT_PROXY.into_array()), client); + + let future_stake = contract + .get_future_stake(public_key.as_bytes().into()) + .call() + .await? + .as_u128(); + + Ok(future_stake) + } + + pub async fn get_stakers(&self) -> Result> { + let client = self.get_signer().await?; + + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .data( + contracts::deposit_v4::GET_STAKERS + .encode_input(&[]) + .unwrap(), + ); + let output = client.call(&tx.into(), None).await.unwrap(); + + let stakers = contracts::deposit_v4::GET_STAKERS + .decode_output(&output) + .unwrap()[0] + .clone() + .into_array() + .unwrap(); + + Ok(stakers + .into_iter() + .map(|k| NodePublicKey::from_bytes(&k.into_bytes().unwrap()).unwrap()) + .collect()) + } + + pub async fn get_reward_address(&self, public_key: &NodePublicKey) -> Result { + let client = self.get_signer().await?; + + let tx = TransactionRequest::new() + .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) + .data( + contracts::deposit_v4::GET_REWARD_ADDRESS + .encode_input(&[Token::Bytes(public_key.as_bytes())]) + .unwrap(), + ); + let output = client.call(&tx.into(), None).await.unwrap(); + + Ok(contracts::deposit_v4::GET_REWARD_ADDRESS + .decode_output(&output) + .unwrap()[0] + .clone() + .into_address() + .unwrap()) + } } #[derive(Debug, Deserialize)] @@ -183,164 +416,3 @@ pub async fn gen_validator_startup_script( Ok(()) } - -async fn build_client( - client_config: &ClientConfig, -) -> Result, LocalWallet>> { - let provider = Provider::::try_from(client_config.chain_endpoint.clone())?; - - let wallet: LocalWallet = client_config - .private_key - .as_str() - .parse::()? - .with_chain_id(provider.get_chainid().await?.as_u64()); - - Ok(SignerMiddleware::new(provider, wallet)) -} - -pub async fn deposit( - validator: &Validator, - client_config: &ClientConfig, - params: &DepositParams, -) -> Result<()> { - println!( - "Deposit: add {} M $ZIL to {}", - params.amount, validator.peer_id - ); - - let client = build_client(client_config).await?; - - // Stake the new validator's funds. - let tx = TransactionRequest::new() - .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) - .value(params.amount as u128 * 1_000_000u128 * 10u128.pow(18)) - .data( - contracts::deposit_v4::DEPOSIT - .encode_input(&[ - Token::Bytes(validator.public_key.as_bytes()), - Token::Bytes(validator.peer_id.to_bytes()), - Token::Bytes(validator.deposit_auth_signature.to_bytes()), - Token::Address(params.reward_address), - Token::Address(params.signing_address), - ]) - .unwrap(), - ); - - // send it! - let pending_tx = client.send_transaction(tx, None).await?; - - // get the mined tx - let receipt = pending_tx - .await? - .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; - let tx = client.get_transaction(receipt.transaction_hash).await?; - - println!("Sent tx: {}\n", serde_json::to_string(&tx)?); - println!("Tx receipt: {}", serde_json::to_string(&receipt)?); - - Ok(()) -} - -pub async fn deposit_top_up( - client_config: &ClientConfig, - bls_public_key: &NodePublicKey, - amount: u8, -) -> Result<()> { - println!("DepositTopUp: add {} M $ZIL stake", amount,); - - let client = build_client(client_config).await?; - - // Topup the validator's funds. - let tx = TransactionRequest::new() - .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) - .value(amount as u128 * 1_000_000u128 * 10u128.pow(18)) - .data( - contracts::deposit_v4::DEPOSIT_TOPUP - .encode_input(&[Token::Bytes(bls_public_key.as_bytes())]) - .unwrap(), - ); - - // send it! - let pending_tx = client.send_transaction(tx, None).await?; - - // get the mined tx - let receipt = pending_tx - .await? - .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; - let tx = client.get_transaction(receipt.transaction_hash).await?; - - println!("Sent tx: {}\n", serde_json::to_string(&tx)?); - println!("Tx receipt: {}", serde_json::to_string(&receipt)?); - - Ok(()) -} - -pub async fn unstake( - client_config: &ClientConfig, - bls_public_key: &NodePublicKey, - amount: u8, -) -> Result<()> { - println!("Unstake: {} M $ZIL unstaked", amount,); - - let client = build_client(client_config).await?; - // Unstake the validator's funds. - let tx = TransactionRequest::new() - .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) - .data( - contracts::deposit_v4::UNSTAKE - .encode_input(&[ - Token::Bytes(bls_public_key.as_bytes()), - Token::Uint((amount as u128 * 1_000_000u128 * 10u128.pow(18)).into()), - ]) - .unwrap(), - ); - - // send it! - let pending_tx = client.send_transaction(tx, None).await?; - - // get the mined tx - let receipt = pending_tx - .await? - .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; - let tx = client.get_transaction(receipt.transaction_hash).await?; - - println!("Sent tx: {}\n", serde_json::to_string(&tx)?); - println!("Tx receipt: {}", serde_json::to_string(&receipt)?); - - Ok(()) -} - -pub async fn withdraw( - client_config: &ClientConfig, - bls_public_key: &NodePublicKey, - count: u8, -) -> Result<()> { - println!("Withdraw: pulling available unstaked funds from deposit contract"); - - let client = build_client(client_config).await?; - // Withdraw the validator's funds. - let tx = TransactionRequest::new() - .to(H160(contract_addr::DEPOSIT_PROXY.into_array())) - .data( - contracts::deposit_v4::UNSTAKE - .encode_input(&[ - Token::Bytes(bls_public_key.as_bytes()), - Token::Uint((count as u128).into()), - ]) - .unwrap(), - ); - - // send it! - let pending_tx = client.send_transaction(tx, None).await?; - - // get the mined tx - let receipt = pending_tx - .await? - .ok_or_else(|| anyhow::anyhow!("tx dropped from mempool"))?; - let tx = client.get_transaction(receipt.transaction_hash).await?; - - println!("Sent tx: {}\n", serde_json::to_string(&tx)?); - println!("Tx receipt: {}", serde_json::to_string(&receipt)?); - - Ok(()) -}