Select Git revision
build-for-arm.md
identity.rs 18.16 KiB
use crate::*;
use crate::commands::vault::retrieve_account_tree_node_for_name;
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", value_name = "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 {
#[clap(value_name = "ADDRESS")]
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 {
#[clap(value_name = "USERNAME")]
target: String,
},
/// Certify an identity
#[clap(alias = "cert")]
Certify {
#[clap(value_name = "USERNAME")]
target: String,
},
/// Renew a certification
#[clap(alias = "renew")]
RenewCert {
#[clap(value_name = "USERNAME")]
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 [target] identity
#[clap(long_about = "Link an account to the [target] identity.\n\
\n\
The target identity can be passed as argument using any of the suggested options.")]
LinkAccount(SecretProvider),
/// Migrate identity to another [target] account
#[clap(long_about = "Migrate identity to another [target] account.\n\
\n\
The target account can be passed as argument using any of the suggested options.")]
ChangeOwnerKey(SecretProvider),
}
#[derive(clap::Args, Clone, Debug)]
pub struct SecretProvider {
/// SS58 Address of target vault account
#[clap(short, conflicts_with_all=["vault_name","secret_format", "secret", "crypto_scheme"])]
address: Option<AccountId>,
/// Name of target vault account
#[clap(short = 'v', conflicts_with_all=["secret_format", "secret", "crypto_scheme"])]
vault_name: Option<String>,
/// Secret key format of target account (seed, substrate)
#[clap(short = 'S', long)]
secret_format: Option<SecretFormat>,
/// Secret of target account
#[clap(short, long)]
secret: Option<String>,
/// Crypto scheme of target account (sr25519, ed25519)
#[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)]
crypto_scheme: CryptoScheme,
}
impl SecretProvider {
/// Analyses the SecretProvider data and tries to retrieve a keypair.
///
/// Will potentially request a password to decrypt the secret in keystore if it was asked to use a vault account.
///
/// Or it could request the `secret` if using `secret-format` without also providing the `secret`.
///
/// Will return an error if no data was provided or if it encountered another issue in the process.
async fn get_keypair(&self, data: &Data) -> Result<KeyPair, GcliError> {
let key_pair = if let Some(address) = self.address.clone() {
commands::vault::fetch_vault_keypair_for_address(data, address).await?
} else if let Some(vault_name) = self.vault_name.clone() {
let account_tree_node =
retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?;
let address = account_tree_node.borrow().account.address.0.clone();
commands::vault::fetch_vault_keypair_for_address(data, address).await?
} else if let Some(secret_format) = self.secret_format {
let keypair = get_keypair(secret_format, self.secret.as_deref(), self.crypto_scheme)?;
println!(
"target address:'{}' (using crypto-scheme:{})",
keypair.address(),
<&'static str>::from(self.crypto_scheme)
);
keypair
} else {
return Err(GcliError::Input(
"One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(),
));
};
Ok(key_pair)
}
}
/// 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).await
}
Subcommand::MemberCount => {
println!(
"member count: {}",
data.client()
.storage()
.at_latest()
.await?
.fetch(&runtime::storage().membership().counter_for_membership(),)
.await?
.unwrap()
)
}
Subcommand::LinkAccount(secret_provider) => {
let target_keypair = secret_provider.get_keypair(&data).await?;
println!("Trying to make the link");
let address = target_keypair.address();
let data = data.fetch_idty_index().await?; // idty index required for payload
link_account(&data, address, target_keypair).await?;
}
Subcommand::ChangeOwnerKey(secret_provider) => {
let target_keypair = secret_provider.get_keypair(&data).await?;
println!("Trying to change owner key");
let address = target_keypair.address();
let data = data.fetch_idty_index().await?; // idty index required for payload
change_owner_key(&data, address, target_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_issued_count: u32,
cert_received: Vec<String>,
cert_received_count: u32,
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: received {}, issued {}",
self.cert_received_count, self.cert_issued_count
)?;
if let Some(smith) = &self.smith {
writeln!(f, "Smith status: {:?}", smith.status)?;
writeln!(
f,
"Smith certs: received {}, issued {}",
smith.cert_received_count, smith.cert_issued_count
)?;
}
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_issued_count: usize,
cert_received: Vec<String>,
cert_received_count: usize,
}
/// 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(|| {
GcliError::Duniter(format!("no identity for account '{account_id}'"))
})?,
// pseudo → idty_id
(None, None, Some(pseudo)) => get_idty_index_by_name(client, pseudo)
.await?
.ok_or_else(|| GcliError::Indexer(format!("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(|| GcliError::Duniter(format!("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(|| {
GcliError::Indexer(format!(
"indexer does not have username for this index {index}"
))
})?
} else {
"<no indexer>".to_string()
});
// get cert meta
let cert_meta = client
.storage()
.at_latest()
.await?
.fetch(
&runtime::storage()
.certification()
.storage_idty_cert_meta(index),
)
.await?
.expect("expected cert meta");
// get certs if possible
let (cert_issued, cert_received, linked_account, smith_info) = if let Some(indexer) = &indexer {
let info = indexer
.identity_info(index)
.await
.ok_or_else(|| GcliError::Indexer(format!("no info for identity {index}")))?;
Ok::<
(
Vec<String>,
Vec<String>,
Vec<AccountId>,
Option<crate::indexer::queries::identity_info::IdentityInfoIdentitySmith>,
),
GcliError,
>((
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,
))
} else {
Ok((vec![], vec![], vec![], None))
}?;
// get smith info
let smith_meta = get_smith(client, index).await?;
let smith = match (smith_meta, smith_info) {
(None, None) => Ok(None),
(Some(s), Some(i)) => Ok(Some(SmithView {
status: s.status,
cert_issued_count: s.issued_certs.len(),
cert_issued: i
.smith_cert_issued
.into_iter()
.map(|i| i.receiver.unwrap().identity.unwrap().name.to_string())
.collect(),
cert_received_count: s.received_certs.len(),
cert_received: i
.smith_cert_received
.into_iter()
.map(|i| i.issuer.unwrap().identity.unwrap().name.to_string())
.collect(),
})),
(Some(s), None) => match indexer {
Some(_) => Err(GcliError::Indexer(format!(
"Duniter and Indexer do not agree if {index} is smith"
))),
None => Ok(Some(SmithView {
status: s.status,
cert_issued_count: s.issued_certs.len(),
cert_issued: s.issued_certs.into_iter().map(|e| e.to_string()).collect(),
cert_received_count: s.received_certs.len(),
cert_received: s
.received_certs
.into_iter()
.map(|e| e.to_string())
.collect(),
})),
},
(None, Some(_)) => Err(GcliError::Indexer(format!(
"Duniter and Indexer do not agree if {pseudo} is smith"
))),
}?;
// 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_issued_count: cert_meta.issued_count,
cert_received,
cert_received_count: cert_meta.received_count,
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,
StaticPayload<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,
StaticPayload<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).await;
// Transform signature to MultiSignature
// TODO: allow other signature formats
let multisign = MultiSignature::Sr25519(signature.into());
submit_call_and_look_event::<
runtime::identity::events::IdtyRemoved,
StaticPayload<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::Ed25519(keypair) => {
let signature = keypair.sign(&payload);
(payload, Signature::Ed25519(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::Ed25519(keypair) => {
let signature = keypair.sign(&payload);
(payload, Signature::Ed25519(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);
// TODO cleaner way to manage signature
let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),
};
submit_call_and_look_event::<
runtime::account::events::AccountLinked,
StaticPayload<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);
// TODO cleaner way to manage signature
let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),
};
submit_call_and_look_event::<
runtime::identity::events::IdtyChangedOwnerKey,
StaticPayload<runtime::identity::calls::types::ChangeOwnerKey>,
>(
data,
&runtime::tx()
.identity()
.change_owner_key(address, signature),
)
.await
}