mod cache; mod indexer; use anyhow::{anyhow, Result}; use clap::Parser; use codec::Encode; use futures::join; use sp_core::{ crypto::{AccountId32, DeriveJunction, Pair as _, Ss58Codec}, sr25519::Pair, }; use std::collections::BTreeMap; use std::str::FromStr; use subxt::ext::sp_runtime::MultiAddress; use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner}; #[subxt::subxt(runtime_metadata_path = "res/metadata.scale")] pub mod gdev_300 {} pub type Client = subxt::OnlineClient<GdevConfig>; pub enum GdevConfig {} impl subxt::config::Config for GdevConfig { type Index = u32; type BlockNumber = u32; type Hash = sp_core::H256; type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256; type AccountId = subxt::ext::sp_runtime::AccountId32; type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>; type Header = subxt::ext::sp_runtime::generic::Header< Self::BlockNumber, subxt::ext::sp_runtime::traits::BlakeTwo256, >; type Signature = subxt::ext::sp_runtime::MultiSignature; type Extrinsic = subxt::ext::sp_runtime::OpaqueExtrinsic; type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>; } #[derive(Copy, Clone, Debug, Default, Encode)] pub struct Tip { #[codec(compact)] tip: u64, } impl Tip { pub fn new(amount: u64) -> Self { Tip { tip: amount } } } impl From<u64> for Tip { fn from(n: u64) -> Self { Self::new(n) } } #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { #[clap(subcommand)] pub subcommand: Subcommand, /// Indexer URL #[clap(short, long, default_value = "https://idx.gdev.cgeek.fr/v1/graphql")] indexer: String, /// Do not use indexer #[clap(long)] no_indexer: bool, /// Secret key or BIP39 mnemonic /// (eventually followed by derivation path) #[clap(short, long)] secret: Option<String>, /// Address #[clap(short, long)] address: Option<String>, /// Websocket RPC endpoint #[clap(short, long, default_value = "ws://localhost:9944")] url: String, } #[derive(Debug, clap::Subcommand)] pub enum Subcommand { CreateOneshot { balance: u64, dest: sp_core::crypto::AccountId32, }, ConsumeOneshot { dest: sp_core::crypto::AccountId32, #[clap(long = "oneshot")] dest_oneshot: bool, }, ConsumeOneshotWithRemaining { balance: u64, dest: sp_core::crypto::AccountId32, #[clap(long = "one")] dest_oneshot: bool, remaining_to: sp_core::crypto::AccountId32, #[clap(long = "rem-one")] remaining_to_oneshot: bool, }, /// List upcoming expirations that require an action Expire { /// Show certs that expire within less than this number of blocks #[clap(short, long, default_value_t = 100800)] blocks: u32, /// Show authorities that should rotate keys within less than this number of sessions #[clap(short, long, default_value_t = 100)] sessions: u32, }, /// Generate a revocation document for the provided account GenRevocDoc, OneshotBalance { account: sp_core::crypto::AccountId32, }, #[clap(hide = true)] Repart { // Number of transactions per block to target target: u32, #[clap(short = 'o', long = "old-repart")] // Old/actual repartition actual_repart: Option<u32>, }, #[clap(hide = true)] SpamRoll { actual_repart: usize }, SudoSetKey { new_key: sp_core::crypto::AccountId32, }, Transfer { balance: u64, dest: sp_core::crypto::AccountId32, #[clap(short = 'k')] keep_alive: bool, }, } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { env_logger::init(); let args = Args::parse(); let (account_id, pair) = match (&args.address, &args.secret) { (Some(address), Some(secret)) => { let pair = Pair::from_string(secret, None) .map_err(|_| anyhow!("Invalid secret {}", secret))?; let address = sp_core::crypto::AccountId32::from_string(address) .map_err(|_| anyhow!("Invalid address {}", address))?; assert_eq!( address, pair.public().into(), "Secret and address do not match." ); (Some(pair.public().into()), Some(pair)) } (None, Some(secret)) => { let pair = Pair::from_string(secret, None) .map_err(|_| anyhow!("Invalid secret {}", secret))?; (Some(pair.public().into()), Some(pair)) } (Some(address), None) => ( Some( sp_core::crypto::AccountId32::from_str(address) .map_err(|_| anyhow!("Invalid address {}", address))?, ), None, ), (None, None) => (None, None), }; let client = Client::new().await.unwrap(); let gql_client = reqwest::Client::builder() .user_agent("gcli/0.1.0") .build()?; if let Some(account_id) = &account_id { let account = client .storage() .fetch(&gdev_300::storage().system().account(account_id), None) .await? .expect("Cannot fetch account"); logs::info!("Account free balance: {}", account.data.free); } match args.subcommand { Subcommand::CreateOneshot { balance, dest } => { let pair = pair.expect("This subcommand needs a secret."); client .tx() .sign_and_submit_then_watch( &gdev_300::tx() .oneshot_account() .create_oneshot_account(dest.into(), balance), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } Subcommand::ConsumeOneshot { dest, dest_oneshot } => { let pair = pair.expect("This subcommand needs a secret."); let number = client .storage() .fetch(&gdev_300::storage().system().number(), None) .await? .unwrap(); client.tx() .sign_and_submit_then_watch(&gdev_300::tx() .oneshot_account() .consume_oneshot_account( number, if dest_oneshot { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Oneshot( dest.into(), ) } else { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Normal( dest.into(), ) }, ), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } Subcommand::ConsumeOneshotWithRemaining { balance, dest, dest_oneshot, remaining_to, remaining_to_oneshot, } => { let pair = pair.expect("This subcommand needs a secret."); let number = client .storage() .fetch(&gdev_300::storage().system().number(), None) .await? .unwrap(); client.tx() .sign_and_submit_then_watch(&gdev_300::tx() .oneshot_account() .consume_oneshot_account_with_remaining( number, if dest_oneshot { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Oneshot( dest.into(), ) } else { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Normal( dest.into(), ) }, if remaining_to_oneshot { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Oneshot( remaining_to.into(), ) } else { gdev_300::runtime_types::pallet_oneshot_account::types::Account::Normal( remaining_to.into(), ) }, balance, ), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } Subcommand::Expire { blocks, sessions } => { let parent_hash = client .storage() .fetch(&gdev_300::storage().system().parent_hash(), None) .await? .unwrap(); let addr_current_block = gdev_300::storage().system().number(); let addr_current_session = gdev_300::storage().session().current_index(); let (current_block, current_session) = join!( client .storage() .fetch(&addr_current_block, Some(parent_hash)), client .storage() .fetch(&addr_current_session, Some(parent_hash),) ); let current_block = current_block?.unwrap(); let current_session = current_session?.unwrap(); let end_block = current_block + blocks; let end_session = current_session + sessions; let mut identity_cache = cache::IdentityCache::new( &client, if args.no_indexer { None } else { Some((&gql_client, &args.indexer)) }, ); // Rotate keys let mut must_rotate_keys_before_iter = client .storage() .iter( gdev_300::storage() .authority_members() .must_rotate_keys_before(0), 10, Some(parent_hash), ) .await?; let mut must_rotate_keys_before = BTreeMap::new(); while let Some((k, v)) = must_rotate_keys_before_iter.next().await? { let session_index = u32::from_le_bytes(k.as_ref()[40..44].try_into().unwrap()); if session_index < end_session { must_rotate_keys_before.insert(session_index - current_session, v); } } println!("\nAuthority members:"); for (sessions_left, identity_ids) in must_rotate_keys_before { println!("Must rotate keys before {} sessions:", sessions_left); for identity_id in identity_ids { println!( " {} ({})", identity_cache .fetch_identity(identity_id, parent_hash) .await .unwrap_or_else(|_| "?".into()), identity_id ); } } // Certifications let mut basic_certs_iter = client .storage() .iter( gdev_300::storage().cert().storage_certs_removable_on(0), 10, Some(parent_hash), ) .await?; let mut basic_certs = BTreeMap::new(); while let Some((k, v)) = basic_certs_iter.next().await? { let block_number = u32::from_le_bytes(k.as_ref()[40..44].try_into().unwrap()); if block_number < end_block { basic_certs.insert(block_number - current_block, v); } } let mut smith_certs_iter = client .storage() .iter( gdev_300::storage() .smiths_cert() .storage_certs_removable_on(0), 10, Some(parent_hash), ) .await?; let mut smith_certs = BTreeMap::new(); while let Some((k, v)) = smith_certs_iter.next().await? { let block_number = u32::from_le_bytes(k.as_ref()[40..44].try_into().unwrap()); if block_number < end_block { smith_certs.insert(block_number - current_block, v); } } for (title, certs) in [ ("Certifications", basic_certs), ("Smith certifications", smith_certs), ] { println!("\n{}:", title); for (blocks_left, certs) in certs { println!("{} blocks before expiration:", blocks_left); for (issuer_id, receiver_id) in certs { println!( " {} ({}) -> {} ({})", identity_cache .fetch_identity(issuer_id, parent_hash) .await .unwrap_or_else(|_| "?".into()), issuer_id, identity_cache .fetch_identity(receiver_id, parent_hash) .await .unwrap_or_else(|_| "?".into()), receiver_id, ); } } } // Memberships let mut basic_membership_iter = client .storage() .iter( gdev_300::storage().membership().memberships_expire_on(0), 10, Some(parent_hash), ) .await?; let mut basic_memberships = BTreeMap::new(); while let Some((k, v)) = basic_membership_iter.next().await? { let block_number = u32::from_le_bytes(k.as_ref()[40..44].try_into().unwrap()); if block_number < end_block { basic_memberships.insert(block_number - current_block, v); } } let mut smith_membership_iter = client .storage() .iter( gdev_300::storage() .smiths_membership() .memberships_expire_on(0), 10, Some(parent_hash), ) .await?; let mut smith_memberships = BTreeMap::new(); while let Some((k, v)) = smith_membership_iter.next().await? { let block_number = u32::from_le_bytes(k.as_ref()[40..44].try_into().unwrap()); if block_number < end_block { smith_memberships.insert(block_number - current_block, v); } } for (title, memberships) in [ ("Memberships", basic_memberships), ("Smith memberships", smith_memberships), ] { println!("\n{}:", title); for (blocks_left, membership) in memberships { println!("{} blocks before expiration:", blocks_left); for identity_id in membership { println!( " {} ({})", identity_cache .fetch_identity(identity_id, parent_hash) .await .unwrap_or_else(|_| "?".into()), identity_id, ); } } } } Subcommand::GenRevocDoc => { gen_revoc_doc(&client, &pair.expect("This subcommand needs a secret.")).await? } Subcommand::OneshotBalance { account } => { logs::info!( "{}", client .storage() .fetch( &gdev_300::storage() .oneshot_account() .oneshot_accounts(&account), None ) .await? .unwrap_or(0) ); } Subcommand::Repart { target, actual_repart, } => { let pair = pair.expect("This subcommand needs a secret."); let mut pairs = Vec::new(); for i in actual_repart.unwrap_or_default()..target { let pair_i = pair .derive(std::iter::once(DeriveJunction::hard::<u32>(i)), None) .map_err(|_| anyhow!("Fail to derive //{}", i))? .0; pairs.push((i, pair_i)); } for (i, pair_i) in &pairs { /*let _ = api .tx() .balances() .transfer(MultiAddress::Id(pair_i.public().into()), 501)? .sign_and_submit_then_watch(&signer, BaseExtrinsicParamsBuilder::new()) .await? .wait_for_in_block() .await?; signer.increment_nonce();*/ if let Some(pair_i_account) = client .storage() .fetch( &gdev_300::storage() .system() .account(&pair_i.public().into()), None, ) .await? { logs::info!("account //{} balance: {}", i, pair_i_account.data.free); } } } Subcommand::SpamRoll { actual_repart } => { let pair = pair.expect("This subcommand needs a secret."); let mut pairs = Vec::<(PairSigner<GdevConfig, Pair>, AccountId32)>::with_capacity(actual_repart); for i in 0..actual_repart { let pair_i = pair .derive(std::iter::once(DeriveJunction::hard::<u32>(i as u32)), None) .map_err(|_| anyhow!("Fail to derive //{}", i))? .0; let account_id_i = pair_i.public().into(); pairs.push((PairSigner::new(pair_i), account_id_i)); } loop { let mut watchers = Vec::with_capacity(actual_repart); for i in 0..(actual_repart - 1) { let dest: AccountId32 = pairs[i + 1].1.clone(); let watcher = client .tx() .sign_and_submit_then_watch( &gdev_300::tx() .balances() .transfer(MultiAddress::Id(dest), 1), &pairs[i].0, BaseExtrinsicParamsBuilder::new(), ) .await?; pairs[i].0.increment_nonce(); logs::info!("send 1 cent from //{} to //{}", i, i + 1); watchers.push(watcher); } let dest: AccountId32 = pairs[0].1.clone(); let watcher = client .tx() .sign_and_submit_then_watch( &gdev_300::tx() .balances() .transfer(MultiAddress::Id(dest), 1), &pairs[actual_repart - 1].0, BaseExtrinsicParamsBuilder::new(), ) .await?; pairs[actual_repart - 1].0.increment_nonce(); logs::info!("send 1 cent from //{} to //0", actual_repart - 1); watchers.push(watcher); // Wait all transactions for watcher in watchers { watcher.wait_for_in_block().await?; } } } Subcommand::SudoSetKey { new_key } => { let pair = pair.expect("This subcommand needs a secret."); client .tx() .sign_and_submit_then_watch( &gdev_300::tx().sudo().set_key(new_key.into()), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } Subcommand::Transfer { balance, dest, keep_alive, } => { let pair = pair.expect("This subcommand needs a secret."); if keep_alive { client .tx() .sign_and_submit_then_watch( &gdev_300::tx().balances().transfer(dest.into(), balance), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } else { client .tx() .sign_and_submit_then_watch( &gdev_300::tx() .balances() .transfer_keep_alive(dest.into(), balance), &PairSigner::new(pair), BaseExtrinsicParamsBuilder::new(), ) .await?; } } } Ok(()) } async fn gen_revoc_doc(api: &Client, pair: &Pair) -> Result<()> { let account_id: sp_core::crypto::AccountId32 = pair.public().into(); let addr_idty_index = gdev_300::storage() .identity() .identity_index_of(&account_id); let addr_block_hash = gdev_300::storage().system().block_hash(0); let (idty_index, genesis_hash) = join!( api.storage().fetch(&addr_idty_index, None,), api.storage().fetch(&addr_block_hash, None) ); let idty_index = idty_index?.unwrap(); let genesis_hash = genesis_hash?.unwrap(); let payload = (b"revo", genesis_hash, idty_index).encode(); let signature = pair.sign(&payload); println!("0x{}", hex::encode(signature)); Ok(()) }