Newer
Older
use crate::{
commands::revocation::generate_revoc_doc,
runtime::runtime_types::{
common_runtime::entities::{IdtyData, NewOwnerKeySignature},
pallet_identity::types::*,
sp_runtime::MultiSignature,
},
};
/// 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")]
#[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: IdtyId },
/// Request distance evaluation
/// make sure that it's ok otherwise currency is slashed
RequestDistanceEvaluation,
/// Get distance status
DistanceStatus,
/// Renew membership
/// When membership comes to and end, it should be renewed for the identity to stay member
RenewMembership,
/// 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>,
}
/// handle identity commands
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// TODO remove indexer where not necessary when BlakeConcat will be there
Subcommand::Show => {}
Subcommand::Get {
ref account_id,
identity_id,
ref username,
} => {
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::RequestDistanceEvaluation => {
commands::distance::request_distance_evaluation(&data).await?;
}
Subcommand::DistanceStatus => {
data = data.fetch_idty_index().await?;
dbg!(commands::distance::get_identity_distance_status(&data).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?;
data = data.fetch_idty_index().await?;
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(),)
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
};
Ok(())
}
// ======================
/// get identity
mut account_id: Option<AccountId>,
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."
));
}
};
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())
);
/// get identity index by account id
.at_latest()
.await?
.fetch(&runtime::storage().identity().identity_index_of(account_id))
idty_index: IdtyId,
) -> Result<Option<IdtyValue<IdtyId, AccountId, IdtyData>>, anyhow::Error> {
.at_latest()
.await?
.fetch(&runtime::storage().identity().identities(idty_index))
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
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(name))
.await
}
pub async fn validate_identity(data: &Data, index: IdtyId) -> Result<(), subxt::Error> {
submit_call_and_look_event::<
runtime::identity::events::IdtyValidated,
Payload<runtime::identity::calls::types::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,
Payload<runtime::membership::calls::types::RenewMembership>,
>(data, &runtime::tx().membership().renew_membership())
.await
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, sr25519::Signature) {
let payload = (b"link", data.genesis_hash, data.idty_index(), address).encode();
let KeyPair::Sr25519(keypair) = keypair else {
panic!("Cesium keys not implemented there")
};
let signature = keypair.sign(&payload);
(payload, 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 = runtime::runtime_types::sp_core::sr25519::Signature(signature.0);
submit_call_and_look_event::<
runtime::account::events::AccountLinked,
Payload<runtime::identity::calls::types::LinkAccount>,
>(
data,
&runtime::tx()
.identity()
.link_account(address, NewOwnerKeySignature(signature)),
)
.await