Newer
Older
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")]
#[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 },
#[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> {
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,
} => {
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?;
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?;
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
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(())
}
// ======================
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// 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,
account_id: Option<AccountId>,
identity_id: Option<IdtyId>,
pseudo: Option<String>,
) -> Result<(), GcliError> {
let client = data.client();
let indexer = data.indexer.clone();
let index =
match (identity_id, &account_id, &pseudo) {
(Some(index), None, None) => index,
// account_id → idty_id
(None, Some(account_id), None) => get_idty_index_by_account_id(client, account_id)
.ok_or_else(|| anyhow!("no identity for account '{account_id}'"))?,
// pseudo → idty_id
(None, None, Some(pseudo)) => get_idty_index_by_name(client, pseudo)
.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)
.ok_or_else(|| anyhow!("no identity value for index {index}"))?;
// pseudo
let pseudo = pseudo.unwrap_or(if let Some(indexer) = &indexer {
.username_by_index(index)
.ok_or_else(|| anyhow!("indexer does not have username for this index {index}"))?
// 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.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,
)
} else {
(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_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))?);
/// get identity index by account id
) -> Result<Option<IdtyId>, subxt::Error> {
client
.at_latest()
.await?
.fetch(&runtime::storage().identity().identity_index_of(account_id))
/// 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())),
)
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}")))
}
) -> Result<Option<IdtyValue<IdtyId, AccountId, IdtyData>>, subxt::Error> {
client
.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,
StaticPayload<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,
StaticPayload<runtime::identity::calls::types::ConfirmIdentity>,
&runtime::tx()
.identity()
.confirm_identity(IdtyName(name.into())),
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,
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,
) -> (LinkAccountPayload, Signature) {
let payload = (b"link", data.genesis_hash, data.idty_index(), address).encode();
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
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,
StaticPayload<runtime::identity::calls::types::LinkAccount>,
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
>(
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,
StaticPayload<runtime::identity::calls::types::ChangeOwnerKey>,
>(
data,
&runtime::tx()
.identity()
.change_owner_key(address, signature),