use crate::*; use crate::{ commands::revocation::generate_revoc_doc, runtime::runtime_types::{ common_runtime::entities::IdtyData, pallet_identity::types::*, pallet_smith_members::types::SmithMeta, pallet_smith_members::SmithStatus, sp_runtime::MultiSignature, }, }; /// define identity subcommands #[derive(Clone, Default, Debug, clap::Parser)] pub enum Subcommand { /// Show identity /// (same as get but without arg) #[default] Show, /// Fetch identity Get { #[clap(short = 'a', long = "address")] account_id: Option<AccountId>, #[clap(short = 'i', long = "identity")] identity_id: Option<IdtyId>, #[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 }, /// Request distance evaluation /// make sure that it's ok otherwise currency is slashed RequestDistanceEvaluation, /// Request distance evaluation for unvalidated identity RequestDistanceEvaluationFor { target: String }, /// Certify an identity #[clap(alias = "cert")] Certify { target: String }, /// Renew a certification #[clap(alias = "renew")] RenewCert { target: String }, /// 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 { /// Secret key format (seed, substrate) #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, /// Secret of account to link /// most likely different from the one owning the identity #[clap(short, long)] secret: Option<String>, }, /// Migrate identity to another account /// Change Owner Key ChangeOwnerKey { /// Secret key format (seed, substrate) #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, /// Secret of account to link /// most likely different from the one owning the identity #[clap(short, long)] secret: Option<String>, }, } /// handle identity commands pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { let mut data = data.build_client().await?; match command { Subcommand::Show => { data = data.build_indexer().await?; get_identity(&data, Some(data.address()), None, None).await? } 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::RequestDistanceEvaluation => { commands::distance::request_distance_evaluation(&data).await?; } Subcommand::RequestDistanceEvaluationFor { target } => { let target = try_get_idty_index_by_name(&data, &target).await?; commands::distance::request_distance_evaluation_for(&data, target).await?; } Subcommand::Certify { target } => { let targetid = try_get_idty_index_by_name(&data, &target).await?; // ask user to confirm certification if let Ok(true) = inquire::Confirm::new(&format!( "Are you sure you want to certify {target} ({targetid})?" )) .with_default(false) .prompt() { commands::certification::certify(&data, targetid).await?; }; } Subcommand::RenewCert { target } => { let target = try_get_idty_index_by_name(&data, &target).await?; commands::certification::renew(&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() .at_latest() .await? .fetch(&runtime::storage().membership().counter_for_membership(),) .await? .unwrap() ) } Subcommand::LinkAccount { secret_format, secret, } => { let keypair = get_keypair(secret_format, secret.as_deref())?; let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload link_account(&data, address, keypair).await?; } Subcommand::ChangeOwnerKey { secret_format, secret, } => { let keypair = get_keypair(secret_format, secret.as_deref())?; let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload change_owner_key(&data, address, keypair).await?; } }; Ok(()) } // ====================== // TODO derive this automatically impl Serialize for IdtyStatus { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { serializer.serialize_str(&format!("{:?}", self)) } } impl Serialize for SmithStatus { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { serializer.serialize_str(&format!("{:?}", self)) } } // for why Arc<[T]> instead of Vec<T> see // https://www.youtube.com/watch?v=A4cKi7PTJSs&pp=ygULVmVjPFN0cmluZz4%3D /// struct to represent details of identity request #[derive(Serialize)] struct IdtyView { index: IdtyId, status: IdtyStatus, pseudo: String, owner_key: AccountId, old_owner_key: Vec<AccountId>, expire_on: BlockNumber, cert_issued: Vec<String>, cert_received: Vec<String>, smith: Option<SmithView>, linked_account: Vec<AccountId>, } impl std::fmt::Display for IdtyView { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "Identity index: {}", self.index)?; writeln!(f, "Username: {}", self.pseudo)?; writeln!( f, "Address: {}", AccountId::to_string(&self.owner_key) )?; writeln!(f, "Status: {:?}", self.status)?; writeln!( f, "Certifications: issued {}, received {}", self.cert_issued.len(), self.cert_received.len() )?; if let Some(smith) = &self.smith { writeln!(f, "Smith status: {:?}", smith.status)?; writeln!( f, "Smith certs: issued {}, received {}", smith.cert_issued.len(), smith.cert_received.len() )?; } let a = self.linked_account.len(); if a > 1 { writeln!(f, "Linked accounts count: {a}")?; } Ok(()) } } #[derive(Serialize)] struct SmithView { status: SmithStatus, cert_issued: Vec<String>, cert_received: Vec<String>, } /// get identity pub async fn get_identity( data: &Data, account_id: Option<AccountId>, identity_id: Option<IdtyId>, pseudo: Option<String>, ) -> Result<(), GcliError> { let client = data.client(); let indexer = data.indexer.clone(); // get idty_id let index = match (identity_id, &account_id, &pseudo) { // idty_id (Some(index), None, None) => index, // account_id → idty_id (None, Some(account_id), None) => get_idty_index_by_account_id(client, account_id) .await? .ok_or_else(|| anyhow!("no identity for account '{account_id}'"))?, // pseudo → idty_id (None, None, Some(pseudo)) => get_idty_index_by_name(client, pseudo) .await? .ok_or_else(|| anyhow!("no identity for name '{pseudo}'"))?, _ => { return Err(GcliError::Logic( "One and only one argument is needed to fetch the identity.".to_string(), )); } }; // idty_id → value let value = get_identity_by_index(client, index) .await? .ok_or_else(|| anyhow!("no identity value for index {index}"))?; // pseudo let pseudo = pseudo.unwrap_or(if let Some(indexer) = &indexer { indexer .username_by_index(index) .await .ok_or_else(|| anyhow!("indexer does not have username for this index {index}"))? } else { "<no indexer>".to_string() }); // get certs if possible let (cert_issued, cert_received, linked_account, smith_cert_issued, smith_cert_received) = if let Some(indexer) = &indexer { let info = indexer.identity_info(index).await.expect("no info"); ( info.cert_issued .into_iter() .map(|i| i.receiver.unwrap().name.to_string()) .collect(), info.cert_received .into_iter() .map(|i| i.issuer.unwrap().name.to_string()) .collect(), info.linked_account .into_iter() .map(|i| AccountId::from_str(&i.id).unwrap()) .collect(), info.smith_cert_issued .into_iter() .map(|i| i.receiver.unwrap().name.to_string()) .collect(), info.smith_cert_received .into_iter() .map(|i| i.issuer.unwrap().name.to_string()) .collect(), ) } else { (vec![], vec![], vec![], vec![], vec![]) }; // get smith info let smith = get_smith(client, index).await?; let smith = smith.map(|s| SmithView { status: s.status, cert_issued: smith_cert_issued, cert_received: smith_cert_received, }); // build view let view = IdtyView { index, status: value.status, pseudo, owner_key: value.owner_key, old_owner_key: vec![], // TODO fetch history of owner key change expire_on: value.next_scheduled, // TODO if zero use membership instead cert_issued, cert_received, smith, linked_account, }; // TODO generic way to do this shared between function match data.args.output_format { OutputFormat::Human => { println!("{view}"); } OutputFormat::Json => { println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); } } Ok(()) } /// get identity index by account id pub async fn get_idty_index_by_account_id( client: &Client, account_id: &AccountId, ) -> Result<Option<IdtyId>, subxt::Error> { client .storage() .at_latest() .await? .fetch(&runtime::storage().identity().identity_index_of(account_id)) .await } /// get smith info by index pub async fn get_smith( client: &Client, index: IdtyId, ) -> Result<Option<SmithMeta<IdtyId>>, subxt::Error> { client .storage() .at_latest() .await? .fetch(&runtime::storage().smith_members().smiths(index)) .await } /// get identity index by name pub async fn get_idty_index_by_name( client: &Client, name: &str, ) -> Result<Option<IdtyId>, subxt::Error> { client .storage() .at_latest() .await? .fetch( &runtime::storage() .identity() .identities_names(IdtyName(name.into())), ) .await } pub async fn try_get_idty_index_by_name(data: &Data, name: &str) -> Result<IdtyId, GcliError> { get_idty_index_by_name(data.client(), name) .await? .ok_or_else(|| GcliError::Input(format!("no identity with name {name}"))) } /// get identityt value by index pub async fn get_identity_by_index( client: &Client, idty_index: IdtyId, ) -> Result<Option<IdtyValue<IdtyId, AccountId, IdtyData>>, subxt::Error> { client .storage() .at_latest() .await? .fetch(&runtime::storage().identity().identities(idty_index)) .await } /// created identity pub async fn create_identity(data: &Data, target: AccountId) -> Result<(), subxt::Error> { submit_call_and_look_event::< runtime::identity::events::IdtyCreated, Payload<runtime::identity::calls::types::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, Payload<runtime::identity::calls::types::ConfirmIdentity>, >( data, &runtime::tx() .identity() .confirm_identity(IdtyName(name.into())), ) .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 = runtime::runtime_types::sp_core::sr25519::Signature(signature.0); let multisign = MultiSignature::Sr25519(signature); submit_call_and_look_event::< runtime::identity::events::IdtyRemoved, Payload<runtime::identity::calls::types::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, keypair: KeyPair, ) -> (LinkAccountPayload, Signature) { let payload = (b"link", data.genesis_hash, data.idty_index(), address).encode(); match keypair { KeyPair::Sr25519(keypair) => { let signature = keypair.sign(&payload); (payload, Signature::Sr25519(signature)) } KeyPair::Nacl(keypair) => { let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); (payload, Signature::Nacl(signature)) } } } type ChOkPayload = Vec<u8>; /// generate link account document pub fn generate_chok_payload( data: &Data, _address: AccountId, keypair: KeyPair, ) -> (ChOkPayload, Signature) { let payload = ( b"icok", data.genesis_hash, data.idty_index(), data.address(), ) .encode(); match keypair { KeyPair::Sr25519(keypair) => { let signature = keypair.sign(&payload); (payload, Signature::Sr25519(signature)) } KeyPair::Nacl(keypair) => { // should not migrate to Nacl let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); (payload, Signature::Nacl(signature)) } } } /// link an account to the identity pub async fn link_account( data: &Data, address: AccountId, keypair: KeyPair, ) -> Result<(), subxt::Error> { let (_payload, signature) = generate_link_account(data, address.clone(), keypair); // this is a hack, see // https://substrate.stackexchange.com/questions/10309/how-to-use-core-crypto-types-instead-of-runtime-types let signature = match signature { Signature::Sr25519(signature) => MultiSignature::Sr25519( runtime::runtime_types::sp_core::sr25519::Signature(signature.0), ), Signature::Nacl(signature) => MultiSignature::Ed25519( runtime::runtime_types::sp_core::ed25519::Signature(signature.try_into().unwrap()), ), }; submit_call_and_look_event::< runtime::account::events::AccountLinked, Payload<runtime::identity::calls::types::LinkAccount>, >( data, &runtime::tx().identity().link_account(address, signature), ) .await } /// change owner key pub async fn change_owner_key( data: &Data, address: AccountId, keypair: KeyPair, ) -> Result<(), subxt::Error> { let (_payload, signature) = generate_chok_payload(data, address.clone(), keypair); // this is a hack, see // https://substrate.stackexchange.com/questions/10309/how-to-use-core-crypto-types-instead-of-runtime-types let signature = match signature { Signature::Sr25519(signature) => MultiSignature::Sr25519( runtime::runtime_types::sp_core::sr25519::Signature(signature.0), ), Signature::Nacl(signature) => MultiSignature::Ed25519( runtime::runtime_types::sp_core::ed25519::Signature(signature.try_into().unwrap()), ), }; submit_call_and_look_event::< runtime::identity::events::IdtyChangedOwnerKey, Payload<runtime::identity::calls::types::ChangeOwnerKey>, >( data, &runtime::tx() .identity() .change_owner_key(address, signature), ) .await }