Skip to content
Snippets Groups Projects
identity.rs 12.3 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::*, 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")]
		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 { 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,
	/// Certify an identity
	Certify { target: IdtyId },
	/// 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 {
		// TODO remove indexer where not necessary when BlakeConcat will be there
		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::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?;
		}
		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)
		}
		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?;
		}
	};

	Ok(())
}

// ======================

/// get identity
pub async fn get_identity(
	data: &Data,
	mut account_id: Option<AccountId>,
	mut identity_id: Option<IdtyId>,
	mut username: Option<String>,
) -> Result<(), anyhow::Error> {
	let client = data.client();
	let indexer = data.indexer.clone();
	// fetch reachable information using Duniter only (no indexer)
	match (&account_id, identity_id, &username) {
		// idty_id → account_id
		(None, Some(identity_id), None) => {
			account_id = get_identity_by_index(client, identity_id)
				.await?
				.map(|idty| idty.owner_key);
			if account_id.is_none() {
				return Err(anyhow!("no identity for this account id"));
			}
		// account_id → idty_id
		(Some(account_id), None, None) => {
			identity_id = get_idty_index_by_account_id(client, account_id).await?;
			if identity_id.is_none() {
				return Err(anyhow!("no identity for this identity index"));
			}
		// username → idty_id and account_id
		(None, None, Some(username)) => {
			identity_id = get_idty_index_by_name(client, username).await?;
			if let Some(identity_id) = identity_id {
				account_id = get_identity_by_index(client, identity_id)
					.await?
					.map(|idty| idty.owner_key);
			} else {
				return Err(anyhow!("no identity found for this username"));
			}
		}
		_ => {
			return Err(anyhow!(
				"One and only one argument is needed to fetch the identity."
			));
		}
	};
	// print result
	println!(
		identity_id.map_or(String::new(), |identity_id| format!("{identity_id}"))
	);
	// 2. username (indexer needed if not provided)
	if let (Some(indexer), Some(identity_id), None) = (&indexer, identity_id, &username) {
		username = indexer.username_by_index(identity_id).await?;
		username.unwrap_or("<no indexer>".to_string())
	);
	// 3. address
	println!(
		"Address:        {}",
		account_id
			.as_ref()
			.map_or(String::new(), AccountId::to_string)
	);
	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))
		.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(name))
		.await
/// 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,
		Payload<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,
		Payload<runtime::identity::calls::types::ConfirmIdentity>,
	>(data, &runtime::tx().identity().confirm_identity(name))
	.await
}
/// confirm identity
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
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);

	// 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, 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::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,
		Payload<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);

	// 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,
		Payload<runtime::identity::calls::types::ChangeOwnerKey>,
	>(
		data,
		&runtime::tx()
			.identity()
			.change_owner_key(address, signature),