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 {
Nicolas80
committed
#[clap(short = 'a', long = "address", value_name = "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.
Nicolas80
committed
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
Nicolas80
committed
RequestDistanceEvaluationFor {
#[clap(value_name = "USERNAME")]
target: String,
},
#[clap(alias = "cert")]
Nicolas80
committed
Certify {
#[clap(value_name = "USERNAME")]
target: String,
},
/// Renew a certification
#[clap(alias = "renew")]
Nicolas80
committed
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 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?;
Nicolas80
committed
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(),)
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(())
}
// ======================
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// 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) {
// 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(),
));
}
};
let value = get_identity_by_index(client, index)
.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}"
))
})?
// 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,
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_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> {
Nicolas80
committed
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,
) -> (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))
}
Nicolas80
committed
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))
}
Nicolas80
committed
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);
let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Nicolas80
committed
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);
let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Nicolas80
committed
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),