-
Nicolas80 authored
* Re-did the mapping of DbAccountId so that we don't have to rely on derive macro FromJsonQueryResult that resulted in using JSON_TEXT in database column instead of VARCHAR * Tricky part I missed the first time was in impl of sea_orm::TryGetable::try_get_by where we should return Err TryGetError::Null in case the value is not present * Was otherwise throwing an exception whenever `parent` field (Option<DbAccountId>) was None when trying to persist the case in DB.
Nicolas80 authored* Re-did the mapping of DbAccountId so that we don't have to rely on derive macro FromJsonQueryResult that resulted in using JSON_TEXT in database column instead of VARCHAR * Tricky part I missed the first time was in impl of sea_orm::TryGetable::try_get_by where we should return Err TryGetError::Null in case the value is not present * Was otherwise throwing an exception whenever `parent` field (Option<DbAccountId>) was None when trying to persist the case in DB.
vault_account.rs 39.55 KiB
use crate::commands::{cesium, vault};
use crate::runtime_config::AccountId;
use crate::utils::GcliError;
use anyhow::anyhow;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::prelude::StringLen;
use sea_orm::ActiveValue::Set;
use sea_orm::PaginatorTrait;
use sea_orm::QueryFilter;
use sea_orm::TryGetError;
use sea_orm::{
ActiveModelBehavior, ColumnTrait, DbErr, DeriveEntityModel, DerivePrimaryKey, EnumIter, Linked,
ModelTrait, QueryOrder, RelationDef, RelationTrait, TryFromU64,
};
use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait};
use sea_orm::{DeriveActiveEnum, EntityTrait};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "vault_account")]
pub struct Model {
/// SS58 Address of account
#[sea_orm(primary_key, auto_increment = false)]
pub address: DbAccountId,
/// Optional name for the account
#[sea_orm(unique)]
pub name: Option<String>,
/// derivation path - None if for a "base" account that has `crypto_scheme` and `encrypted_suri` set and no `parent`
pub path: Option<String>,
/// Crypto scheme used for the account - Only set for "base" accounts
pub crypto_scheme: Option<DbCryptoScheme>,
/// Encrypted SURI for the account - Only set for "base" accounts
pub encrypted_suri: Option<Vec<u8>>,
/// ForeignKey to parent vault_account SS58 Address - None if for a "base" account
pub parent: Option<DbAccountId>,
}
impl Model {
pub fn is_base_account(&self) -> bool {
self.parent.is_none()
}
#[allow(unused)]
pub fn is_derivation_account(&self) -> bool {
self.parent.is_some()
}
pub fn account_type(&self) -> String {
if self.is_base_account() {
"Base".to_string()
} else {
"Derivation".to_string()
}
}
}
impl Display for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_base_account() {
match self.crypto_scheme {
None => {
unreachable!()
}
Some(DbCryptoScheme::Ed25519) => {
// Also showing G1v1 public key for Ed25519 (base) accounts
write!(
f,
"{}[address:{}, g1v1_pub_key:{}, name:{:?}, crypto_scheme:{:?}]",
self.account_type(),
self.address,
cesium::compute_g1v1_public_key_from_ed25519_account_id(&self.address.0),
self.name,
self.crypto_scheme
)
}
Some(DbCryptoScheme::Sr25519) => {
write!(
f,
"{}[address:{}, name:{:?}, crypto_scheme:{:?}]",
self.account_type(),
self.address,
self.name,
self.crypto_scheme
)
}
}
} else {
fn get_parent_name(parent: &Option<DbAccountId>) -> String {
if let Some(parent) = parent {
format!("Some(\"{parent}\")")
} else {
"None".to_string()
}
}
write!(
f,
"{}[address:{}, name:{:?}, path:{:?}, parent:{}]",
self.account_type(),
self.address,
self.name,
self.path,
get_parent_name(&self.parent)
)
}
}
}
/// Necessary to create a wrapper over AccountId to implement sea-orm traits
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DbAccountId(pub AccountId);
/// All the next methods are necessary to support the proper mapping of the DbAccountId from/to String in DB and
/// to allow using it as primaryKey
impl sea_orm::sea_query::Nullable for DbAccountId {
fn null() -> Value {
Value::String(None)
}
}
impl sea_orm::TryGetable for DbAccountId {
/// Had to really pay attention to return proper TryGetError type when value not present => TryGetError::Null
///
/// as otherwise, when using `Option<DbAccountId>` with a None value it was crashing (when no "parent")
fn try_get_by<I: sea_orm::ColIdx>(res: &QueryResult, idx: I) -> Result<Self, TryGetError> {
let value: String = res
.try_get_by(idx)
.map_err(|e| TryGetError::Null(e.to_string()))?;
Ok(DbAccountId(AccountId::from_str(&value).map_err(|e| {
TryGetError::DbErr(DbErr::Custom(e.to_string()))
})?))
}
}
impl sea_orm::sea_query::ValueType for DbAccountId {
fn try_from(v: Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
Value::String(Some(value)) => Ok(DbAccountId(
AccountId::from_str(&value).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
)),
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
stringify!(DbAccountId).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::String
}
fn column_type() -> ColumnType {
ColumnType::String(StringLen::None)
}
}
impl From<DbAccountId> for Value {
fn from(account_id: DbAccountId) -> Self {
Value::String(Some(Box::new(account_id.0.to_string())))
}
}
/// sea-orm forces us to implement this one; but since we map from/to string, we can't convert from a u64
impl TryFromU64 for DbAccountId {
fn try_from_u64(_v: u64) -> Result<Self, DbErr> {
Err(DbErr::Custom(
"AccountIdWrapper cannot be created from U64".to_owned(),
))
}
}
impl Display for DbAccountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for DbAccountId {
type Err = GcliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(DbAccountId(
AccountId::from_str(s).map_err(|e| GcliError::Input(e.to_string()))?,
))
}
}
impl From<String> for DbAccountId {
fn from(s: String) -> Self {
DbAccountId(AccountId::from_str(&s).expect("Invalid AccountId format"))
}
}
impl From<AccountId> for DbAccountId {
fn from(account_id: AccountId) -> Self {
DbAccountId(account_id)
}
}
impl From<DbAccountId> for AccountId {
fn from(db_account_id: DbAccountId) -> Self {
db_account_id.0
}
}
/// Didn't want to pollute the keys::CryptoScheme enum with sea-orm specific derivations
///
/// created a separate enum for the database with conversions between the two
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(
rs_type = "String",
db_type = "String(StringLen::None)",
rename_all = "PascalCase"
)]
pub enum DbCryptoScheme {
Ed25519,
Sr25519,
}
impl From<crate::keys::CryptoScheme> for DbCryptoScheme {
fn from(scheme: crate::keys::CryptoScheme) -> Self {
match scheme {
crate::keys::CryptoScheme::Ed25519 => DbCryptoScheme::Ed25519,
crate::keys::CryptoScheme::Sr25519 => DbCryptoScheme::Sr25519,
}
}
}
impl From<DbCryptoScheme> for crate::keys::CryptoScheme {
fn from(scheme: DbCryptoScheme) -> Self {
match scheme {
DbCryptoScheme::Ed25519 => crate::keys::CryptoScheme::Ed25519,
DbCryptoScheme::Sr25519 => crate::keys::CryptoScheme::Sr25519,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
ParentAccount,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::ParentAccount => Entity::belongs_to(Entity)
.from(Column::Parent)
.to(Column::Address)
.into(),
}
}
}
pub struct ParentAccountLink;
impl Linked for ParentAccountLink {
type FromEntity = Entity;
type ToEntity = Entity;
fn link(&self) -> Vec<RelationDef> {
vec![Relation::ParentAccount.def()]
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
/// This method is called before saving or updating the model to the database.
/// It ensures that the model is valid according to the following constraints:
///
/// - A "base" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)
/// - A "derivation" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if insert {
// If one of the elements of a "base" account is seen, all must be correctly filled
if (self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none())
|| (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none())
|| (self.crypto_scheme.is_set()
&& self.crypto_scheme.try_as_ref().unwrap().is_some())
|| (self.encrypted_suri.is_set()
&& self.encrypted_suri.try_as_ref().unwrap().is_some())
{
if !((self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none())
&& (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none())
&& (self.crypto_scheme.is_set()
&& self.crypto_scheme.try_as_ref().unwrap().is_some())
&& (self.encrypted_suri.is_set()
&& self.encrypted_suri.try_as_ref().unwrap().is_some()))
{
return Err(DbErr::Custom(
"A \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(),
));
}
} else if !((self.path.is_set() && self.path.try_as_ref().unwrap().is_some())
&& (self.parent.is_set() && self.parent.try_as_ref().unwrap().is_some())
&& (self.crypto_scheme.is_not_set()
|| self.crypto_scheme.try_as_ref().unwrap().is_none())
&& (self.encrypted_suri.is_not_set()
|| self.encrypted_suri.try_as_ref().unwrap().is_none()))
{
return Err(DbErr::Custom(
"A \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(),
));
}
} else {
//Updates to accept:
// * Name Only
// * Overriding Base with Base account => only changing encrypted_suri
// * Should also support changing name at the same time
// * Overriding Derivation with Base account => upd Path=>None Parent=>None crypto=>Some enc_suri=>Some
// * Should also support changing name at the same time
// * Overriding Derivation with Derivation account => upd (Path=>Some) (Parent=>Some)
// * Should also support changing name at the same time
// * Overriding Base with Derivation account => upd Path=>Some Parent=>Some crypto=>None enc_suri=>None
// * Should also support changing name at the same time
// If updating all path, parent, crypto_scheme, encrypted_suri
if self.path.is_set()
&& self.parent.is_set()
&& self.crypto_scheme.is_set()
&& self.encrypted_suri.is_set()
{
if self.parent.try_as_ref().unwrap().is_some() {
if !(self.path.try_as_ref().unwrap().is_some()
&& self.crypto_scheme.try_as_ref().unwrap().is_none()
&& self.encrypted_suri.try_as_ref().unwrap().is_none())
{
return Err(DbErr::Custom(
"An update to \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(),
));
}
} else if !(self.path.try_as_ref().unwrap().is_none()
&& self.crypto_scheme.try_as_ref().unwrap().is_some()
&& self.encrypted_suri.try_as_ref().unwrap().is_some())
{
return Err(DbErr::Custom(
"An update to \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(),
));
}
}
// Else if updating path || parent both needs to have Some(_) value (update of Derivation)
else if (self.path.is_set() || self.parent.is_set())
&& !(self.parent.try_as_ref().unwrap().is_some()
&& self.path.try_as_ref().unwrap().is_some())
{
return Err(DbErr::Custom(
"An update of \"derivation\" parent/path must have both path:Some(_), parent:Some(_)".into(),
));
}
}
Ok(self)
}
}
pub async fn find_by_id<C>(db: &C, address: &DbAccountId) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find_by_id(address.clone())
.one(db)
.await
.map_err(GcliError::from)
}
pub async fn find_by_name<C>(db: &C, name: &str) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Name.eq(Some(name.to_string())))
.one(db)
.await
.map_err(GcliError::from)
}
pub async fn find_base_accounts<C>(db: &C) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Path.is_null())
.order_by_asc(Column::Address)
.all(db)
.await
.map_err(GcliError::from)
}
/// Represents a node in the hierarchy of accounts
pub struct AccountTreeNode {
pub account: Model,
pub children: Vec<Rc<RefCell<AccountTreeNode>>>,
pub parent: Option<Rc<RefCell<AccountTreeNode>>>,
}
/// Counts the depth of an `AccountTreeNode` in the hierarchy.
pub fn count_depth_account_tree_node(account_tree_node: &Rc<RefCell<AccountTreeNode>>) -> usize {
let mut depth = 0;
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
depth += 1;
current_node = parent_node;
}
depth
}
/// Counts number of accounts in an `AccountTreeNode` hierarchy, starting from account_tree_node and only visiting children.
pub fn count_accounts_in_account_tree_node_and_children(
node: &Rc<RefCell<AccountTreeNode>>,
) -> usize {
let borrowed_node = node.borrow();
let mut count = 1; // Count the current node
for child in &borrowed_node.children {
count += count_accounts_in_account_tree_node_and_children(child);
}
count
}
/// Gets the base account tree node of the `AccountTreeNode` hierarchy.
pub fn get_base_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Rc<RefCell<AccountTreeNode>> {
//Move up to the base node
let mut base_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = base_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
base_node = parent_node;
}
Rc::clone(&base_node)
}
/// Gets the account tree node for given address from the `AccountTreeNode` hierarchy.
pub fn get_account_tree_node_for_address(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
address: &str,
) -> Rc<RefCell<AccountTreeNode>> {
fn find_address_recursive(
node: &Rc<RefCell<AccountTreeNode>>,
address: &str,
) -> Option<Rc<RefCell<AccountTreeNode>>> {
let borrowed_node = node.borrow();
if borrowed_node.account.address.to_string() == address {
return Some(Rc::clone(node));
}
for child in &borrowed_node.children {
if let Some(found) = find_address_recursive(child, address) {
return Some(found);
}
}
None
}
//Move up to the base node
let base_account_tree_node = get_base_account_tree_node(account_tree_node);
let account_tree_node_for_address = find_address_recursive(&base_account_tree_node, address)
.unwrap_or_else(|| {
panic!(
"Could not find account with address:{} in the hierarchy",
address
)
});
Rc::clone(&account_tree_node_for_address)
}
/// Returns a new (limited) `AccountTreeNode` hierarchy including the selected account_tree_node and all its parents.
///
/// The base of the new hierarchy will be returned
pub fn get_base_parent_hierarchy_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Rc<RefCell<AccountTreeNode>> {
// Clone the current node to start the new hierarchy
let new_node = Rc::new(RefCell::new(AccountTreeNode {
account: account_tree_node.borrow().account.clone(),
children: Vec::new(),
parent: None,
}));
// Traverse up to the base node, creating new nodes for each parent
let mut current_new_node = Rc::clone(&new_node);
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
let new_parent_node = Rc::new(RefCell::new(AccountTreeNode {
account: parent_node.borrow().account.clone(),
children: vec![Rc::clone(¤t_new_node)],
parent: None,
}));
current_new_node.borrow_mut().parent = Some(Rc::clone(&new_parent_node));
current_new_node = new_parent_node;
current_node = parent_node;
}
// Return the base of the new hierarchy
current_new_node
}
/// Returns a vec of all the accounts starting from `account_tree_node` and all its children; depth first
///
/// Can be used to delete all the accounts in the hierarchy in the proper order
pub fn extract_accounts_depth_first_from_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Result<Vec<Model>, GcliError> {
fn retrieve_recursive_depth_first(
node: &Rc<RefCell<AccountTreeNode>>,
accounts: &mut Vec<Model>,
) -> Result<(), GcliError> {
let borrowed_node = node.borrow();
for child in &borrowed_node.children {
retrieve_recursive_depth_first(child, accounts)?;
}
accounts.push(borrowed_node.account.clone());
Ok(())
}
let mut accounts = Vec::new();
retrieve_recursive_depth_first(account_tree_node, &mut accounts)?;
Ok(accounts)
}
/// Computes the name to reference the `AccountTreeNode` in the hierarchy if we can find/compute one.
///
/// Returns `None` otherwise.
pub fn compute_name_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Option<String> {
let mut name = String::new();
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
if let Some(account_name) = &borrowed_node.account.name {
name.insert_str(0, account_name);
return Some(name);
} else if let Some(account_path) = &borrowed_node.account.path {
name.insert_str(0, account_path);
} else {
return None;
}
borrowed_node.parent.as_ref().map(Rc::clone)
} {
current_node = parent_node;
}
Some(name)
}
/// Computes a map of names to reference of `AccountTreeNodes` in the hierarchy of its children.
pub fn compute_name_map_for_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Result<HashMap<String, Rc<RefCell<AccountTreeNode>>>, GcliError> {
let mut names_to_ref_map = HashMap::<String, Rc<RefCell<AccountTreeNode>>>::new();
fn compute_recursive_name_map(
node: &Rc<RefCell<AccountTreeNode>>,
current_name: Option<String>,
names_to_ref_map: &mut HashMap<String, Rc<RefCell<AccountTreeNode>>>,
) -> Result<(), GcliError> {
let borrowed_node = node.borrow();
let current_name = match &borrowed_node.account.name {
Some(name) => Some(name.clone()),
None => match &borrowed_node.account.path {
Some(path) => current_name
.as_ref()
.map(|name| format!("{}{}", name, path)),
None => None,
},
};
if let Some(name) = ¤t_name {
names_to_ref_map.insert(name.clone(), Rc::clone(node));
}
for child in &borrowed_node.children {
compute_recursive_name_map(child, current_name.clone(), names_to_ref_map)?;
}
Ok(())
}
compute_recursive_name_map(account_tree_node, None, &mut names_to_ref_map)?;
Ok(names_to_ref_map)
}
/// Computes the SURI of the `AccountTreeNode` in the hierarchy; using the password to decrypt the encrypted SURI of Base account.
pub fn compute_suri_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
password: String,
) -> Result<String, GcliError> {
let mut suri = String::new();
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
if let Some(account_path) = &borrowed_node.account.path {
suri.insert_str(0, account_path);
} else if let Some(encrypted_suri) = &borrowed_node.account.encrypted_suri {
let decrypted_suri = vault::decrypt(encrypted_suri, password.clone())
.map_err(|e| GcliError::Input(e.to_string()))?;
let secret_suri = String::from_utf8(decrypted_suri).map_err(|e| anyhow!(e))?;
suri.insert_str(0, &secret_suri);
} else {
return Err(GcliError::Input("No encrypted SURI found".to_string()));
}
borrowed_node.parent.as_ref().map(Rc::clone)
} {
current_node = parent_node;
}
Ok(suri)
}
/// Fetches all the `base` account tree nodes with their hierarchies
pub async fn fetch_all_base_account_tree_node_hierarchies<C>(
db: &C,
) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
let base_accounts = find_base_accounts(db).await?;
let mut account_tree_nodes = Vec::new();
for base_account in base_accounts {
let base_account_tree_node =
fetch_children_account_tree_nodes_boxed(db, base_account, None).await?;
account_tree_nodes.push(base_account_tree_node);
}
Ok(account_tree_nodes)
}
/// Only returns the `base` accounts without their children
///
/// To be used in compute_vault_accounts_table to display only the base accounts
pub async fn fetch_only_base_account_tree_nodes<C>(
db: &C,
) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
let base_accounts = find_base_accounts(db).await?;
let mut account_tree_nodes = Vec::new();
for base_account in base_accounts {
let current_node = Rc::new(RefCell::new(AccountTreeNode {
account: base_account.clone(),
children: Vec::new(),
parent: None,
}));
account_tree_nodes.push(current_node);
}
Ok(account_tree_nodes)
}
/// Fetches the `base` account tree node hierarchy for the given address
///
/// This one unwraps the Option and gives a proper error message in case of None
pub async fn fetch_base_account_tree_node_hierarchy_unwrapped<C>(
db: &C,
address: &str,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
fetch_base_account_tree_node_hierarchy(db, address)
.await?
.ok_or(GcliError::Input(format!(
"Could not compute tree of accounts for address:'{}'",
address
)))
}
/// Fetches the `base` account tree node hierarchy for the given address using db
pub async fn fetch_base_account_tree_node_hierarchy<C>(
db: &C,
address: &str,
) -> Result<Option<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
if let Some(base_parent_account) = find_base_parent_account(db, address).await? {
let base_account_tree_node =
fetch_children_account_tree_nodes_boxed(db, base_parent_account, None).await?;
Ok(Some(base_account_tree_node))
} else {
Ok(None)
}
}
/// Finds the `base` account in db for the given address
async fn find_base_parent_account<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
let account = find_by_id(
db,
&DbAccountId::from_str(address).expect("invalid address"),
)
.await?;
if account.is_none() {
return Ok(None);
}
let mut base_parent_account = account.unwrap();
while let Some(parent_account) = base_parent_account
.find_linked(ParentAccountLink)
.one(db)
.await
.map_err(GcliError::from)?
{
base_parent_account = parent_account;
}
Ok(Some(base_parent_account))
}
async fn find_direct_children_accounts<C>(
db: &C,
current_account: &Model,
) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Parent.eq(current_account.address.clone()))
.order_by_asc(Column::Address)
.all(db)
.await
.map_err(GcliError::from)
}
/// To make clippy happy... "warning: very complex type used. Consider factoring parts into `type` definitions"
type AccountTreeNodeResult<'c> =
Pin<Box<dyn Future<Output = Result<Rc<RefCell<AccountTreeNode>>, GcliError>> + 'c>>;
/// This one seems necessary in order to handle async + recursion issue
///
/// Was suggested by AI and seems to work (might be improved)
fn fetch_children_account_tree_nodes_boxed<'c, C>(
db: &'c C,
current_account: Model,
parent_node: Option<Rc<RefCell<AccountTreeNode>>>,
) -> AccountTreeNodeResult<'c>
where
C: ConnectionTrait + 'c,
{
Box::pin(fetch_children_account_tree_nodes(
db,
current_account,
parent_node,
))
}
/// Fetches the children account tree nodes for the given account and parent node
async fn fetch_children_account_tree_nodes<C>(
db: &C,
current_account: Model,
parent_node: Option<Rc<RefCell<AccountTreeNode>>>,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
let children_accounts = find_direct_children_accounts(db, ¤t_account).await?;
let current_node = Rc::new(RefCell::new(AccountTreeNode {
account: current_account.clone(),
children: Vec::new(),
parent: parent_node,
}));
let mut children = Vec::new();
for child_account in children_accounts {
let child_node = fetch_children_account_tree_nodes_boxed(
db,
child_account,
Some(Rc::clone(¤t_node)),
)
.await?;
children.push(child_node);
}
current_node.borrow_mut().children = children;
Ok(current_node)
}
pub async fn check_name_available<C>(
db: &C,
old_name: Option<&String>,
new_name: Option<&String>,
) -> Result<bool, GcliError>
where
C: ConnectionTrait,
{
if old_name == new_name {
return Ok(true);
}
if let Some(new_name) = new_name {
let name_usage_count = Entity::find()
.filter(Column::Name.eq(Some(new_name.clone())))
.count(db)
.await?;
Ok(name_usage_count == 0)
} else {
Ok(true)
}
}
pub async fn update_account<C>(db: &C, vault_account: ActiveModel) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
Ok(vault_account.update(db).await?)
}
/// Creates a `base` vault account and returns it
///
/// Typically used for `vault import|migrate` commands
pub async fn create_base_account<C>(
db: &C,
address: &str,
name: Option<&String>,
crypto_scheme: crate::keys::CryptoScheme,
encrypted_suri: Vec<u8>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let account_id = DbAccountId::from_str(address)?;
let existing_vault_account = Entity::find_by_id(account_id.clone()).one(db).await?;
Ok(match existing_vault_account {
Some(existing_vault_account) => {
// To be safe
return Err(GcliError::Input(format!(
"Already existing vault account {existing_vault_account}"
)));
}
None => {
let vault_account = ActiveModel {
address: Set(account_id),
name: Set(name.cloned()),
path: Set(None),
crypto_scheme: Set(Some(crypto_scheme.into())),
encrypted_suri: Set(Some(encrypted_suri)),
parent: Default::default(),
};
vault_account.insert(db).await?
}
})
}
/// Creates a `derivation` vault account and returns it
///
/// Typically used for `vault derive` command
pub async fn create_derivation_account<C>(
db: &C,
address: &str,
name: Option<&String>,
derivation_path: &str,
parent_address: &str,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let account_id = DbAccountId::from_str(address)?;
let vault_account = Entity::find_by_id(account_id.clone()).one(db).await?;
Ok(match vault_account {
Some(vault_account) => {
// To be safe
return Err(GcliError::Input(format!(
"Already existing vault account {vault_account}"
)));
}
None => {
let vault_account = ActiveModel {
address: Set(address.to_string().into()),
name: Set(name.cloned()),
path: Set(Some(derivation_path.to_string())),
crypto_scheme: Set(None),
encrypted_suri: Set(None),
parent: Set(Some(parent_address.to_string().into())),
};
vault_account.insert(db).await?
}
})
}
pub async fn update_account_name<C>(
db: &C,
account: Model,
new_name: Option<&String>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let old_name = account.name.clone();
let mut account: ActiveModel = account.into();
account.name = Set(new_name.cloned());
let account = account.update(db).await?;
println!(
"Renamed address:'{}' from {:?} to {:?}",
&account.address, old_name, new_name
);
Ok(account)
}
// Unit tests
#[cfg(test)]
pub mod tests {
use super::*;
pub mod account_tree_node_tests {
use super::*;
use crate::commands::vault;
use crate::keys;
use crate::keys::SUBSTRATE_MNEMONIC;
pub fn mother_account_tree_node() -> Rc<RefCell<AccountTreeNode>> {
let mother_address =
DbAccountId::from_str("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV").unwrap();
let child1_address =
DbAccountId::from_str("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH").unwrap();
let grandchild1_address =
DbAccountId::from_str("5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d").unwrap();
let child2_address =
DbAccountId::from_str("5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o").unwrap();
let grandchild2_address =
DbAccountId::from_str("5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT").unwrap();
let grandchild1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: grandchild1_address.clone(),
name: Some("Grandchild 1".to_string()),
path: Some("//0".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(child1_address.clone()),
},
children: vec![],
parent: None,
}));
let grandchild2 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: grandchild2_address.clone(),
// name: Some("Grandchild 2".to_string()),
name: None,
path: Some("//1".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(child2_address.clone()),
},
children: vec![],
parent: None,
}));
let child1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: child1_address.clone(),
name: Some("Child 1".to_string()),
path: Some("//0".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(mother_address.clone()),
},
children: vec![grandchild1.clone()],
parent: None,
}));
let child2 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: child2_address.clone(),
// name: Some("Child 2".to_string()),
name: None,
path: Some("//1".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(mother_address.clone()),
},
children: vec![grandchild2.clone()],
parent: None,
}));
let mother = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: mother_address.clone(),
name: Some("Mother".to_string()),
path: None,
crypto_scheme: Some(DbCryptoScheme::Sr25519),
encrypted_suri: Some(
vault::encrypt(SUBSTRATE_MNEMONIC.as_bytes(), "".to_string()).unwrap(),
),
parent: None,
},
children: vec![child1.clone(), child2.clone()],
parent: None,
}));
// Set parent references
grandchild1.borrow_mut().parent = Some(child1.clone());
grandchild2.borrow_mut().parent = Some(child2.clone());
child1.borrow_mut().parent = Some(mother.clone());
child2.borrow_mut().parent = Some(mother.clone());
mother
}
pub fn mother_g1v1_account_tree_node() -> Rc<RefCell<AccountTreeNode>> {
let mother_address =
DbAccountId::from_str("5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4").unwrap();
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
let seed = keys::seed_from_cesium(&cesium_id, &cesium_pwd);
let secret_suri = format!("0x{}", hex::encode(seed));
let mother_g1v1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: mother_address.clone(),
name: Some("MotherG1v1".to_string()),
path: None,
crypto_scheme: Some(DbCryptoScheme::Ed25519),
encrypted_suri: Some(
vault::encrypt(secret_suri.as_bytes(), "".to_string()).unwrap(),
),
parent: None,
},
children: vec![],
parent: None,
}));
mother_g1v1
}
#[test]
fn test_count_depth_account_tree_node() {
let mother = mother_account_tree_node();
assert_eq!(count_depth_account_tree_node(&mother), 0);
let child1 = mother.borrow().children[0].clone();
assert_eq!(count_depth_account_tree_node(&child1), 1);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(count_depth_account_tree_node(&grandchild1), 2);
}
#[test]
fn test_count_accounts_in_account_tree_node_and_children() {
let mother = mother_account_tree_node();
assert_eq!(count_accounts_in_account_tree_node_and_children(&mother), 5);
let child1 = mother.borrow().children[0].clone();
assert_eq!(count_accounts_in_account_tree_node_and_children(&child1), 2);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
count_accounts_in_account_tree_node_and_children(&grandchild1),
1
);
}
#[test]
fn test_get_base_parent_hierarchy_account_tree_node() {
let mother = mother_account_tree_node();
let child1 = mother.borrow().children[0].clone();
let new_mother = get_base_parent_hierarchy_account_tree_node(&child1);
// Check if the base of the new hierarchy is the mother node
assert_eq!(
new_mother.borrow().account.address.to_string(),
mother.borrow().account.address.to_string()
);
assert_eq!(new_mother.borrow().children.len(), 1);
// Check if the child1 node is correctly linked in the new hierarchy
let new_child1 = new_mother.borrow().children[0].clone();
assert_eq!(
new_child1.borrow().account.address.to_string(),
child1.borrow().account.address.to_string()
);
assert_eq!(new_child1.borrow().children.len(), 0);
// Check if the parent references are correctly set
assert!(new_mother.borrow().parent.is_none());
assert_eq!(
new_child1
.borrow()
.parent
.as_ref()
.unwrap()
.borrow()
.account
.address
.to_string(),
new_mother.borrow().account.address.to_string()
);
}
#[test]
fn test_retrieve_accounts_depth_first_from_account_tree_node() {
let mother = mother_account_tree_node();
let accounts = extract_accounts_depth_first_from_account_tree_node(&mother).unwrap();
assert_eq!(accounts.len(), 5);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[1].address.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[2].address.to_string(),
mother.borrow().children[1].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[3].address.to_string(),
mother.borrow().children[1]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[4].address.to_string(),
mother.borrow().account.address.to_string()
);
let child1 = mother.borrow().children[0].clone();
let accounts = extract_accounts_depth_first_from_account_tree_node(&child1).unwrap();
assert_eq!(accounts.len(), 2);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[1].address.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
let grandchild1 = child1.borrow().children[0].clone();
let accounts =
extract_accounts_depth_first_from_account_tree_node(&grandchild1).unwrap();
assert_eq!(accounts.len(), 1);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
}
#[test]
fn test_compute_name_account_tree_node() {
let mother = mother_account_tree_node();
assert_eq!(
compute_name_account_tree_node(&mother),
Some("Mother".to_string())
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Grandchild 1".to_string())
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(
compute_name_account_tree_node(&child2),
Some("Mother//1".to_string())
);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild2),
Some("Mother//1//1".to_string())
);
}
#[test]
fn test_compute_name_account_tree_node_mother_without_name() {
let mother = mother_account_tree_node();
mother.borrow_mut().account.name = None;
assert_eq!(compute_name_account_tree_node(&mother), None);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Grandchild 1".to_string())
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(compute_name_account_tree_node(&child2), None);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(compute_name_account_tree_node(&grandchild2), None);
}
#[test]
fn test_compute_name_account_tree_node_grandchild1_without_name() {
let mother = mother_account_tree_node();
mother.borrow().children[0].borrow().children[0]
.borrow_mut()
.account
.name = None;
assert_eq!(
compute_name_account_tree_node(&mother),
Some("Mother".to_string())
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Child 1//0".to_string())
);
}
#[test]
fn test_compute_name_map_for_account_tree_node() {
let mother = mother_account_tree_node();
let name_map = compute_name_map_for_account_tree_node(&mother).unwrap();
assert_eq!(name_map.len(), 5);
assert_eq!(
name_map
.get("Mother")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().account.address.to_string()
);
assert_eq!(
name_map
.get("Child 1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Grandchild 1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Mother//1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[1]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Mother//1//1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[1].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
}
#[test]
fn test_get_base_account_tree_node() {
let mother = mother_account_tree_node();
let child1 = mother.borrow().children[0].clone();
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
get_base_account_tree_node(&grandchild1)
.borrow()
.account
.address
.to_string(),
mother.borrow().account.address.to_string()
);
}
#[test]
fn test_get_account_tree_node_for_address() {
let mother = mother_account_tree_node();
let grandchild1 = mother.borrow().children[0].borrow().children[0].clone();
let grandchild1_address = &grandchild1.borrow().account.address.to_string();
assert_eq!(
get_account_tree_node_for_address(&mother, grandchild1_address)
.borrow()
.account
.address
.to_string(),
grandchild1_address.to_string()
);
}
#[test]
fn test_compute_suri_account_tree_node() {
let mother = mother_account_tree_node();
let password = "".to_string();
assert_eq!(
compute_suri_account_tree_node(&mother, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&child1, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//0"
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&grandchild1, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//0//0"
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(
compute_suri_account_tree_node(&child2, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//1"
);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&grandchild2, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//1//1"
);
}
}
}