Skip to content
Snippets Groups Projects
identity.rs 16.1 KiB
Newer Older
Hugo Trentesaux's avatar
Hugo Trentesaux committed
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", 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
	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 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> {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	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,
		} => {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
			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(),)
		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?;
		}
// 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: 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_received: Vec<String>,
	cert_received_count: usize,
/// get identity
pub async fn get_identity(
	data: &Data,
	account_id: Option<AccountId>,
	identity_id: Option<IdtyId>,
) -> 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)
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		.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");

	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))
	}?;
	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))?);
	Ok(())
/// get identity index by account id
pub async fn get_idty_index_by_account_id(
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	client: &Client,
	account_id: &AccountId,
) -> Result<Option<IdtyId>, subxt::Error> {
	client
		.storage()
		.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}")))
}

/// get identityt value by index
pub async fn get_identity_by_index(
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	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))
/// created identity
pub async fn create_identity(data: &Data, target: AccountId) -> Result<(), subxt::Error> {
	submit_call_and_look_event::<
		runtime::identity::events::IdtyCreated,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		StaticPayload<runtime::identity::calls::types::ConfirmIdentity>,
	>(
		data,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		&runtime::tx()
			.identity()
			.confirm_identity(IdtyName(name.into())),
Hugo Trentesaux's avatar
Hugo Trentesaux committed
/// 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
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// TODO: allow other signature formats
	let multisign = MultiSignature::Sr25519(signature.into());
	submit_call_and_look_event::<
		runtime::identity::events::IdtyRemoved,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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);
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// TODO cleaner way to manage signature
	let signature = match signature {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
		Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),

	submit_call_and_look_event::<
		runtime::account::events::AccountLinked,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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);

Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// TODO cleaner way to manage signature
	let signature = match signature {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
		Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),
	};

	submit_call_and_look_event::<
		runtime::identity::events::IdtyChangedOwnerKey,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		StaticPayload<runtime::identity::calls::types::ChangeOwnerKey>,
	>(
		data,
		&runtime::tx()
			.identity()
			.change_owner_key(address, signature),