From 058d89d2cf0e5f27f0f26ffd1a7f31a830d9bd35 Mon Sep 17 00:00:00 2001 From: scx1332 Date: Fri, 29 Mar 2024 13:26:27 +0100 Subject: [PATCH] Added distributor contract for more robust testing --- .github/workflows/distribute.yml | 53 +++++++++ crates/erc20_payment_lib/config-payments.toml | 1 + .../contracts/distributor.json | 69 ++++++++++++ crates/erc20_payment_lib/src/config.rs | 27 +++-- crates/erc20_payment_lib/src/contracts.rs | 25 ++++- crates/erc20_payment_lib/src/runtime.rs | 103 +++++++++++++++++- crates/erc20_payment_lib/src/setup.rs | 6 + crates/erc20_payment_lib/src/transaction.rs | 23 ++++ .../src/config_setup.rs | 1 + .../src/rpc_pool/eth_generic_call.rs | 7 +- prometheus/metrics.txt | 17 ++- src/main.rs | 73 ++++++++++++- src/options.rs | 32 ++++++ 13 files changed, 417 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/distribute.yml create mode 100644 crates/erc20_payment_lib/contracts/distributor.json diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml new file mode 100644 index 00000000..ed6c2934 --- /dev/null +++ b/.github/workflows/distribute.yml @@ -0,0 +1,53 @@ +name: Distribute tests + +on: + push: + workflow_dispatch: + schedule: + - cron: "42 3 * * *" + +jobs: + test_deposit: + name: Test Distribute + timeout-minutes: 20 + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + shared-key: "dev-build-cache" + + - name: Build + run: | + cargo build + cp target/debug/erc20_processor /usr/local/bin/erc20_processor + [ $(which erc20_processor) == "/usr/local/bin/erc20_processor" ] + + - name: Generate ethereum accounts + run: | + erc20_processor generate-key -n 5 > .env + cat .env | grep ETH_ADDRESS | sed "s/#\s//g" | sed "s/:\s/=/g" > $GITHUB_ENV + + - name: Distribute ETH + run: | + erc20_processor distribute --amounts "0.0001;0.0001;0.0001;0.0001;0.0001" --recipients "$ETH_ADDRESS_0;$ETH_ADDRESS_1;$ETH_ADDRESS_2;$ETH_ADDRESS_3;$ETH_ADDRESS_4" + erc20_processor run + env: + ETH_PRIVATE_KEYS: ${{ secrets.HOLESKY_FUND_ENV }} + + - name: Transfer all left ETH tokens + run: | + set -x + erc20_processor show-config > config.toml.tmp + sed 's/^max-fee-per-gas = "20"$/max-fee-per-gas = "1.1"/' config.toml.tmp > config-payments.toml + erc20_processor transfer --account-no 0 --recipient 0x0079dce233830c7b0cd41116214e17b93c64e030 --token eth --all + erc20_processor transfer --account-no 1 --recipient 0x0079dce233830c7b0cd41116214e17b93c64e030 --token eth --all + erc20_processor transfer --account-no 2 --recipient 0x0079dce233830c7b0cd41116214e17b93c64e030 --token eth --all + erc20_processor transfer --account-no 3 --recipient 0x0079dce233830c7b0cd41116214e17b93c64e030 --token eth --all + erc20_processor transfer --account-no 4 --recipient 0x0079dce233830c7b0cd41116214e17b93c64e030 --token eth --all + erc20_processor run diff --git a/crates/erc20_payment_lib/config-payments.toml b/crates/erc20_payment_lib/config-payments.toml index 282f7d26..9ed2c729 100644 --- a/crates/erc20_payment_lib/config-payments.toml +++ b/crates/erc20_payment_lib/config-payments.toml @@ -143,6 +143,7 @@ token = { address = "0x8888888815bf4DB87e57B609A50f938311EEd068", symbol = "tGLM multi-contract = { address = "0xAaAAAaA00E1841A63342db7188abA84BDeE236c7", max-at-once = 10 } mint-contract = { address = "0xFACe100969FF47EB58d2CF603321B581A84bcEaC", max-glm-allowed = 400 } lock-contract = { address = "0xfe1B27Bac0e3Ad39d55C9459ae59894De847dcbf" } +distributor-contract = { address = "0xb7Fb99e86f93dc3047A12932052236d853065173" } faucet-client = { max-eth-allowed = 0.009, faucet-srv = "_holesky-faucet._tcp", faucet-host = "faucet.testnet.golem.network", faucet-lookup-domain = "dev.golem.network", faucet-srv-port = 4002 } confirmation-blocks = 0 block-explorer-url = "https://holesky.etherscan.io" diff --git a/crates/erc20_payment_lib/contracts/distributor.json b/crates/erc20_payment_lib/contracts/distributor.json new file mode 100644 index 00000000..ef2b227d --- /dev/null +++ b/crates/erc20_payment_lib/contracts/distributor.json @@ -0,0 +1,69 @@ +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "addrs", + "type": "bytes" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "distribute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "addrs", + "type": "bytes" + } + ], + "name": "distributeEqual", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "addrs", + "type": "bytes" + }, + { + "internalType": "uint32[]", + "name": "values", + "type": "uint32[]" + } + ], + "name": "distributeEther", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "addrs", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "values", + "type": "uint64[]" + } + ], + "name": "distributeGwei", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/crates/erc20_payment_lib/src/config.rs b/crates/erc20_payment_lib/src/config.rs index 89603757..473c3d1e 100644 --- a/crates/erc20_payment_lib/src/config.rs +++ b/crates/erc20_payment_lib/src/config.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::btree_map::BTreeMap as Map; use rust_decimal::Decimal; @@ -46,7 +46,7 @@ impl AdditionalOptions { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct Engine { pub process_interval: u64, @@ -63,33 +63,39 @@ pub struct Engine { pub ignore_deadlines: bool, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Config { pub chain: Map, pub engine: Engine, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct MultiContractSettings { pub address: Address, pub max_at_once: usize, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct MintContractSettings { pub address: Address, pub max_glm_allowed: Decimal, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct LockContractSettings { pub address: Address, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct DistributorContractSettings { + pub address: Address, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct FaucetClientSettings { pub max_eth_allowed: Decimal, @@ -99,7 +105,7 @@ pub struct FaucetClientSettings { pub faucet_lookup_domain: String, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct RpcSettings { pub names: Option, @@ -115,7 +121,7 @@ pub struct RpcSettings { pub max_consecutive_errors: Option, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct Chain { pub chain_name: String, @@ -128,6 +134,7 @@ pub struct Chain { pub multi_contract: Option, pub mint_contract: Option, pub lock_contract: Option, + pub distributor_contract: Option, pub faucet_client: Option, pub transaction_timeout: u64, pub confirmation_blocks: u64, @@ -138,7 +145,7 @@ pub struct Chain { pub external_source_check_interval: Option, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Token { pub symbol: String, pub address: Address, diff --git a/crates/erc20_payment_lib/src/contracts.rs b/crates/erc20_payment_lib/src/contracts.rs index 5c44831d..3e6e4a15 100644 --- a/crates/erc20_payment_lib/src/contracts.rs +++ b/crates/erc20_payment_lib/src/contracts.rs @@ -7,7 +7,7 @@ use web3::contract::tokens::Tokenize; use web3::contract::Contract; use web3::transports::Http; use web3::types::{Address, U256}; -use web3::{Transport, Web3}; +use web3::{ethabi, Transport, Web3}; // todo remove DUMMY_RPC_PROVIDER and use ABI instead // todo change to once_cell @@ -26,6 +26,8 @@ lazy_static! { }; pub static ref LOCK_CONTRACT_TEMPLATE: Contract = prepare_contract_template(include_bytes!("../contracts/lock_payments.json")).unwrap(); + pub static ref DISTRIBUTOR_CONTRACT_TEMPLATE: Contract = + prepare_contract_template(include_bytes!("../contracts/distributor.json")).unwrap(); } pub fn prepare_contract_template(json_abi: &[u8]) -> Result, PaymentError> { @@ -72,6 +74,27 @@ pub fn encode_erc20_allowance( contract_encode(&ERC20_CONTRACT_TEMPLATE, "allowance", (owner, spender)) } +pub fn encode_distribute( + recipients: &[Address], + amounts: &[U256], +) -> Result, web3::ethabi::Error> { + if recipients.len() != amounts.len() { + return Err(web3::ethabi::Error::InvalidData); + } + let mut bytes = Vec::with_capacity(recipients.len() * 20); + for recipient in recipients { + bytes.extend_from_slice(recipient.as_bytes()); + } + // convert to abi encoded bytes + let bytes = ethabi::Bytes::from(bytes); + + contract_encode( + &DISTRIBUTOR_CONTRACT_TEMPLATE, + "distribute", + (bytes, amounts.to_vec()), + ) +} + pub fn encode_faucet_create() -> Result, web3::ethabi::Error> { contract_encode(&FAUCET_CONTRACT_TEMPLATE, "create", ()) } diff --git a/crates/erc20_payment_lib/src/runtime.rs b/crates/erc20_payment_lib/src/runtime.rs index e6d2eeda..e56e86e5 100644 --- a/crates/erc20_payment_lib/src/runtime.rs +++ b/crates/erc20_payment_lib/src/runtime.rs @@ -1,7 +1,7 @@ use crate::signer::{Signer, SignerAccount}; use crate::transaction::{ - create_create_deposit, create_faucet_mint, create_terminate_deposit, create_token_transfer, - find_receipt_extended, FindReceiptParseResult, + create_create_deposit, create_distribute_transaction, create_faucet_mint, + create_terminate_deposit, create_token_transfer, find_receipt_extended, FindReceiptParseResult, }; use crate::{err_custom_create, err_from}; use erc20_payment_lib_common::create_sqlite_connection; @@ -951,6 +951,32 @@ impl PaymentRuntime { Ok(()) } + pub async fn distribute_gas( + &self, + chain_name: &str, + from: Address, + ) -> Result<(), PaymentError> { + let chain_cfg = self.config.chain.get(chain_name).ok_or(err_custom_create!( + "Chain {} not found in config file", + chain_name + ))?; + let golem_address = chain_cfg.token.address; + let web3 = self.setup.get_provider(chain_cfg.chain_id)?; + + let res = mint_golem_token( + web3, + &self.conn, + chain_cfg.chain_id as u64, + from, + golem_address, + chain_cfg.mint_contract.clone().map(|c| c.address), + false, + ) + .await; + self.wake.notify_one(); + res + } + pub async fn mint_golem_token( &self, chain_name: &str, @@ -1038,6 +1064,79 @@ impl VerifyTransactionResult { } } +#[allow(clippy::too_many_arguments)] +pub async fn distribute_gas( + web3: Arc, + conn: &SqlitePool, + chain_id: u64, + from: Address, + distribute_contract_address: Option
, + skip_balance_check: bool, + recipients: &[Address], + amounts: &[rust_decimal::Decimal], +) -> Result<(), PaymentError> { + let distribute_contract_address = + if let Some(distribute_contract_address) = distribute_contract_address { + distribute_contract_address + } else { + return Err(err_custom_create!( + "Distribute contract address unknown. If not sure try on holesky network" + )); + }; + + if recipients.len() != amounts.len() { + return Err(err_custom_create!( + "recipients and amounts must have the same length" + )); + } + + let mut amounts_u256: Vec = Vec::with_capacity(amounts.len()); + + let mut sum_u256 = U256::zero(); + for amount in amounts { + let amount = amount + .to_u256_from_eth() + .map_err(|err| err_custom_create!("Invalid amount: {} - {}", amount, err))?; + amounts_u256.push(amount); + sum_u256 += amount; + } + + if !skip_balance_check { + //todo check if we have enough gas + token to distribute + let get_eth_balance = web3 + .clone() + .eth_balance(from, None) + .await + .map_err(err_from!())? + .to_eth_saturate(); + + if get_eth_balance < Decimal::from_f64(0.000001).unwrap() { + return Err(err_custom_create!( + "You need at least 0.000001 ETH to continue. You have {} ETH on network with chain id: {} and account {:#x} ", + get_eth_balance, + chain_id, + from + )); + } + } + + let distribute_tx = create_distribute_transaction( + from, + distribute_contract_address, + chain_id, + None, + recipients, + &amounts_u256, + )?; + let distribute_tx = insert_tx(conn, &distribute_tx).await.map_err(err_from!())?; + + log::info!( + "Distribute transaction added to queue: {}", + distribute_tx.id + ); + Ok(()) +} + pub async fn mint_golem_token( web3: Arc, conn: &SqlitePool, diff --git a/crates/erc20_payment_lib/src/setup.rs b/crates/erc20_payment_lib/src/setup.rs index 555680c4..88a2862d 100644 --- a/crates/erc20_payment_lib/src/setup.rs +++ b/crates/erc20_payment_lib/src/setup.rs @@ -51,6 +51,7 @@ pub struct ChainSetup { pub glm_address: Address, pub multi_contract_address: Option
, pub lock_contract_address: Option
, + pub distribute_contract_address: Option
, pub faucet_setup: FaucetSetup, pub multi_contract_max_at_once: usize, pub transaction_timeout: u64, @@ -301,6 +302,11 @@ impl PaymentSetup { .map(|m| m.max_at_once) .unwrap_or(1), lock_contract_address: chain_config.1.lock_contract.clone().map(|m| m.address), + distribute_contract_address: chain_config + .1 + .distributor_contract + .clone() + .map(|m| m.address), faucet_setup, transaction_timeout: chain_config.1.transaction_timeout, diff --git a/crates/erc20_payment_lib/src/transaction.rs b/crates/erc20_payment_lib/src/transaction.rs index c8eab597..399f6807 100644 --- a/crates/erc20_payment_lib/src/transaction.rs +++ b/crates/erc20_payment_lib/src/transaction.rs @@ -327,6 +327,29 @@ pub fn create_erc20_transfer_multi(multi_args: MultiTransferArgs) -> Result, + recipients: &[Address], + amounts: &[U256], +) -> Result { + let sum_amounts = amounts.iter().fold(U256::zero(), |acc, x| acc + x); + Ok(TxDbObj { + method: "DISTRIBUTOR.distribute".to_string(), + from_addr: format!("{from:#x}"), + to_addr: format!("{faucet_address:#x}"), + chain_id: chain_id as i64, + gas_limit: gas_limit.map(|gas_limit| gas_limit as i64), + call_data: Some(hex::encode( + encode_distribute(recipients, amounts).map_err(err_from!())?, + )), + val: sum_amounts.to_string(), + ..Default::default() + }) +} + pub fn create_faucet_mint( from: Address, faucet_address: Address, diff --git a/crates/erc20_payment_lib_test/src/config_setup.rs b/crates/erc20_payment_lib_test/src/config_setup.rs index d20fa19d..104326e4 100644 --- a/crates/erc20_payment_lib_test/src/config_setup.rs +++ b/crates/erc20_payment_lib_test/src/config_setup.rs @@ -42,6 +42,7 @@ pub async fn create_default_config_setup(proxy_url_base: &str, proxy_key: &str) }), mint_contract: None, lock_contract: None, + distributor_contract: None, faucet_client: None, transaction_timeout: 25, confirmation_blocks: 1, diff --git a/crates/erc20_rpc_pool/src/rpc_pool/eth_generic_call.rs b/crates/erc20_rpc_pool/src/rpc_pool/eth_generic_call.rs index 58ea03b4..df8177ab 100644 --- a/crates/erc20_rpc_pool/src/rpc_pool/eth_generic_call.rs +++ b/crates/erc20_rpc_pool/src/rpc_pool/eth_generic_call.rs @@ -116,7 +116,12 @@ impl Web3RpcPool { } return Err(web3::Error::Rpc(e)); } else { - log::warn!("Unknown RPC error: {}", e); + log::warn!( + "Unknown RPC error when calling {} from endpoint {}: {}", + EthMethodCall::METHOD, + self.get_name(idx), + e + ); self.mark_rpc_error( idx, EthMethodCall::METHOD.to_string(), diff --git a/prometheus/metrics.txt b/prometheus/metrics.txt index 650f4a38..a6fd8426 100644 --- a/prometheus/metrics.txt +++ b/prometheus/metrics.txt @@ -1,6 +1,15 @@ -# HELP senders_count Number of distinct receivers -# TYPE senders_count counter -senders_count{chain_id="137", sender="0x09e4f0ae44d5e60d44a8928af7531e6a862290bc"} 19 +# HELP receivers_count Number of distinct receivers +# TYPE receivers_count counter +receivers_count{chain_id="17000", receiver="0x001111a27323e8fba0176393d03714c0f7467e2b"} 30 # HELP erc20_transferred Number of distinct receivers # TYPE erc20_transferred counter -erc20_transferred{chain_id="137", sender="0x09e4f0ae44d5e60d44a8928af7531e6a862290bc"} 0.000000029482970684 +erc20_transferred{chain_id="17000", sender="0x001111a27323e8fba0176393d03714c0f7467e2b"} 0 +# HELP payment_count Number of distinct payments +# TYPE payment_count counter +payment_count{chain_id="17000", sender="0x001111a27323e8fba0176393d03714c0f7467e2b"} 0 +# HELP transaction_count Number of web3 transactions +# TYPE transaction_count counter +transaction_count{chain_id="17000", sender="0x001111a27323e8fba0176393d03714c0f7467e2b"} 4 +# HELP fee_paid Total fee paid +# TYPE fee_paid counter +fee_paid{chain_id="17000", sender="0x001111a27323e8fba0176393d03714c0f7467e2b"} 0 diff --git a/src/main.rs b/src/main.rs index a912284d..5c47372d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,8 +35,8 @@ use crate::stats::{export_stats, run_stats}; use erc20_payment_lib::faucet_client::faucet_donate; use erc20_payment_lib::misc::gen_private_keys; use erc20_payment_lib::runtime::{ - get_token_balance, mint_golem_token, remove_last_unsent_transactions, remove_transaction_force, - PaymentRuntimeArgs, + distribute_gas, get_token_balance, mint_golem_token, remove_last_unsent_transactions, + remove_transaction_force, PaymentRuntimeArgs, }; use erc20_payment_lib::server::web::{runtime_web_scope, ServerData}; use erc20_payment_lib::setup::PaymentSetup; @@ -75,6 +75,7 @@ async fn main_internal() -> Result<(), PaymentError> { PaymentCommands::MintTestTokens { .. } => {} PaymentCommands::Deposit { .. } => {} PaymentCommands::Transfer { .. } => {} + PaymentCommands::Distribute { .. } => {} PaymentCommands::Balance { .. } => {} PaymentCommands::ImportPayments { .. } => {} PaymentCommands::ScanBlockchain { .. } => {} @@ -82,6 +83,7 @@ async fn main_internal() -> Result<(), PaymentError> { PaymentCommands::ExportHistory { .. } => {} PaymentCommands::DecryptKeyStore { .. } => {} PaymentCommands::Cleanup { .. } => {} + PaymentCommands::ShowConfig { .. } => {} } let (private_keys, public_addrs) = if private_key_load_needed { @@ -276,6 +278,65 @@ async fn main_internal() -> Result<(), PaymentError> { } => { check_rpc_local(check_web3_rpc_options, config).await?; } + PaymentCommands::Distribute { distribute_options } => { + let public_addr = if let Some(address) = distribute_options.address { + address + } else if let Some(account_no) = distribute_options.account_no { + *public_addrs + .get(account_no) + .expect("No public adss found with specified account_no") + } else { + *public_addrs.first().expect("No public adss found") + }; + let chain_cfg = + config + .chain + .get(&distribute_options.chain_name) + .ok_or(err_custom_create!( + "Chain {} not found in config file", + distribute_options.chain_name + ))?; + + let payment_setup = PaymentSetup::new_empty(&config)?; + let web3 = payment_setup.get_provider(chain_cfg.chain_id)?; + + let mut recipients = Vec::with_capacity(distribute_options.recipients.len()); + + for recipient in distribute_options.recipients.split(';') { + let recipient = recipient.trim(); + recipients.push(check_address_name(recipient).map_err(|e| { + err_custom_create!("Invalid recipient address {}, {}", recipient, e) + })?); + } + + let amounts = distribute_options + .amounts + .split(';') + .map(|s| { + let s = s.trim(); + Decimal::from_str(s) + .map_err(|e| err_custom_create!("Invalid amount {}, {}", s, e)) + }) + .collect::, PaymentError>>()?; + + if amounts.len() != recipients.len() { + return Err(err_custom_create!( + "Number of recipients and amounts must be the same" + )); + } + + distribute_gas( + web3, + &conn.clone().unwrap(), + chain_cfg.chain_id as u64, + public_addr, + chain_cfg.distributor_contract.clone().map(|c| c.address), + false, + &recipients, + &amounts, + ) + .await?; + } PaymentCommands::GetDevEth { get_dev_eth_options, } => { @@ -727,6 +788,14 @@ async fn main_internal() -> Result<(), PaymentError> { } } } + PaymentCommands::ShowConfig => { + println!( + "{}", + toml::to_string_pretty(&config).map_err(|err| err_custom_create!( + "Something went wrong when serializing to json {err}" + ))? + ); + } } if let Some(conn) = conn.clone() { diff --git a/src/options.rs b/src/options.rs index 1c2bdb39..8bf87944 100644 --- a/src/options.rs +++ b/src/options.rs @@ -172,6 +172,33 @@ pub struct WithdrawTokensOptions { pub skip_balance_check: bool, } +#[derive(StructOpt)] +#[structopt(about = "Distribute token (gas) options")] +pub struct DistributeOptions { + #[structopt(short = "c", long = "chain-name", default_value = "holesky")] + pub chain_name: String, + + #[structopt(long = "address", help = "Address (has to have private key)")] + pub address: Option
, + + #[structopt(long = "account-no", help = "Address by index (for convenience)")] + pub account_no: Option, + + #[structopt( + short = "r", + long = "recipients", + help = "Recipient (semicolon separated)" + )] + pub recipients: String, + + #[structopt( + short = "a", + long = "amounts", + help = "Amounts (decimal, full precision, i.e. 0.01;0.002, separate by semicolon)" + )] + pub amounts: String, +} + #[derive(StructOpt)] #[structopt(about = "Single transfer options")] pub struct TransferOptions { @@ -422,6 +449,10 @@ pub enum PaymentCommands { #[structopt(flatten)] single_transfer_options: TransferOptions, }, + Distribute { + #[structopt(flatten)] + distribute_options: DistributeOptions, + }, Balance { #[structopt(flatten)] account_balance_options: BalanceOptions, @@ -450,6 +481,7 @@ pub enum PaymentCommands { #[structopt(flatten)] cleanup_options: CleanupOptions, }, + ShowConfig, } #[derive(StructOpt)]