use crate::*; use crate::{ commands::revocation::generate_revoc_doc, runtime::runtime_types::{ common_runtime::entities::{IdtyData, NewOwnerKeySignature}, pallet_identity::types::*, sp_core::sr25519::Signature, sp_runtime::MultiSignature, }, }; use std::str::FromStr; /// define identity subcommands #[derive(Clone, Default, Debug, clap::Parser)] pub enum Subcommand { /// show identity #[default] Show, /// Fetch identity Get { #[clap(short = 'a', long = "address")] account_id: Option<AccountId>, #[clap(short = 'i', long = "identity")] identity_id: Option<u32>, #[clap(short = 'u', long = "username")] username: Option<String>, }, /// Create and certify an identity /// /// Caller must be member, and the target account must exist. Create { target: AccountId }, /// Confirm an identity /// /// To be called by the certified not-yet-member account, to become member. Confirm { name: String }, /// Validate an identity /// Should be called when the distance has been evaluated positively Validate { index: u32 }, /// Renew membership /// When membership comes to and end, it should be renewed for the identity to stay member RenewMembership, /// Certify an identity Certify { target: u32 }, /// Revoke an identity immediately Revoke, /// Generate a revocation document for the provided account GenRevocDoc, /// Display member count MemberCount, /// Link an account to the identity LinkAccount { /// address of the account that has to be linked address: AccountId, }, } /// handle identity commands pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { let mut data = data.build_client().await?; match command { Subcommand::Show => {} Subcommand::Get { ref account_id, identity_id, ref username, } => { data = data.build_indexer().await?; get_identity(&data, account_id.clone(), identity_id, username.clone()).await? } Subcommand::Create { target } => { create_identity(&data, target).await?; } Subcommand::Confirm { name } => { confirm_identity(&data, name).await?; } Subcommand::Validate { index } => { validate_identity(&data, index).await?; } Subcommand::RenewMembership => { renew_membership(&data).await?; } Subcommand::Certify { target } => { data = data.fetch_idty_index().await?; // TODO fetch target username / key / index // and ask user to confirm certification commands::certification::certify(&data, target).await?; } Subcommand::Revoke => { data = data.fetch_idty_index().await?; revoke_identity(&data).await?; } Subcommand::GenRevocDoc => { data = data.fetch_idty_index().await?; commands::revocation::print_revoc_sig(&data) } Subcommand::MemberCount => { println!( "member count: {}", data.client() .storage() .fetch( &runtime::storage().membership().counter_for_membership(), None, ) .await? .unwrap() ) } Subcommand::LinkAccount { address } => { data = data.fetch_idty_index().await?; // idty index required for payload link_account(&data, address).await?; } }; Ok(()) } // ====================== /// get identity pub async fn get_identity( data: &Data, mut account_id: Option<AccountId>, mut identity_id: Option<u32>, mut username: Option<String>, ) -> Result<(), anyhow::Error> { let client = data.client(); let indexer = data.indexer.clone(); // fetch missing information match (&account_id, identity_id, &username) { (None, Some(identity_id), None) => { account_id = get_identity_by_index(client, identity_id) .await? .map(|idty| idty.owner_key); } (Some(account_id), None, None) => { identity_id = get_idty_index_by_account_id(client, account_id).await?; } (None, None, Some(username)) => { let indexer = indexer.as_ref().ok_or(anyhow!( "Cannot fetch identity from username without indexer." ))?; if let Some(pubkey) = indexer.pubkey_by_username(username).await? { // convert string to accountid let fetched_account_id = AccountId::from_str(&pubkey).map_err(|e| anyhow!(e))?; // in the future, also ask indexer the identity index identity_id = get_idty_index_by_account_id(client, &fetched_account_id).await?; account_id = Some(fetched_account_id); } else { return Err(anyhow!("no identity found for this username")); } } _ => { return Err(anyhow!( "One and only one argument is needed to fetch the identity." )); } }; // print result println!( "Account id: {}", account_id .as_ref() .map_or(String::new(), AccountId::to_string) ); println!( "Identity id: {}", identity_id.map_or(String::new(), |identity_id| format!("{identity_id}")) ); if let (Some(indexer), Some(account_id), None) = (&indexer, &account_id, &username) { username = indexer.username_by_pubkey(&account_id.to_string()).await?; } println!( "Username: {}", username.unwrap_or("<no indexer>".to_string()) ); Ok(()) } /// get identity index by account id pub async fn get_idty_index_by_account_id( client: &Client, account_id: &AccountId, ) -> Result<Option<u32>, anyhow::Error> { Ok(client .storage() .fetch( &runtime::storage().identity().identity_index_of(account_id), None, ) .await?) } /// get identityt value by index pub async fn get_identity_by_index( client: &Client, idty_index: u32, ) -> Result<Option<IdtyValue<u32, AccountId, IdtyData>>, anyhow::Error> { Ok(client .storage() .fetch(&runtime::storage().identity().identities(idty_index), None) .await?) } /// created identity pub async fn create_identity(data: &Data, target: AccountId) -> Result<(), subxt::Error> { submit_call_and_look_event::< runtime::identity::events::IdtyCreated, StaticTxPayload<runtime::identity::calls::CreateIdentity>, >(data, &runtime::tx().identity().create_identity(target)) .await } /// confirm identity pub async fn confirm_identity(data: &Data, name: String) -> Result<(), subxt::Error> { submit_call_and_look_event::< runtime::identity::events::IdtyConfirmed, StaticTxPayload<runtime::identity::calls::ConfirmIdentity>, >(data, &runtime::tx().identity().confirm_identity(name)) .await } /// confirm identity pub async fn validate_identity(data: &Data, index: u32) -> Result<(), subxt::Error> { submit_call_and_look_event::< runtime::identity::events::IdtyValidated, StaticTxPayload<runtime::identity::calls::ValidateIdentity>, >(data, &runtime::tx().identity().validate_identity(index)) .await } /// renew membership pub async fn renew_membership(data: &Data) -> Result<(), subxt::Error> { submit_call_and_look_event::< runtime::membership::events::MembershipRenewed, StaticTxPayload<runtime::membership::calls::RenewMembership>, >(data, &runtime::tx().membership().renew_membership()) .await } /// generate revokation document and submit it immediately pub async fn revoke_identity(data: &Data) -> Result<(), subxt::Error> { let (_payload, signature) = generate_revoc_doc(&data); // Transform signature to MultiSignature // TODO: this is a hack, we should be able to use the signature directly let signature = Signature(signature.0); let multisign = MultiSignature::Sr25519(signature); submit_call_and_look_event::< runtime::identity::events::IdtyRemoved, StaticTxPayload<runtime::identity::calls::RevokeIdentity>, >( data, &runtime::tx() .identity() .revoke_identity(data.idty_index(), data.address(), multisign), ) .await } type LinkAccountPayload = Vec<u8>; /// generate link account document pub fn generate_link_account( data: &Data, address: AccountId, ) -> (LinkAccountPayload, sp_core::sr25519::Signature) { let payload = (b"link", data.genesis_hash, data.idty_index(), address).encode(); let signature = data.keypair().sign(&payload); (payload, signature) } /// link an account to the identity pub async fn link_account(data: &Data, address: AccountId) -> Result<(), subxt::Error> { let (_payload, signature) = generate_link_account(data, address.clone()); // this is a hack, see // https://substrate.stackexchange.com/questions/10309/how-to-use-core-crypto-types-instead-of-runtime-types let signature = Signature(signature.0); submit_call_and_look_event::< runtime::account::events::AccountLinked, StaticTxPayload<runtime::identity::calls::LinkAccount>, >( data, &runtime::tx() .identity() .link_account(address, NewOwnerKeySignature(signature)), ) .await }