mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-13 18:59:26 +00:00
* feat: add basic commands for communicating with keycard * initialize changes * reorganization * add script file for easier wallet access * update commands * fixes * fixed load for non continuous run * Updates for signatures with keycard * fix BIP-340 signatures for fixed sized messages * fmt * refactor and add pin support to program facades * fix unit test * fixes * Revert "fixes" This reverts commit 41f34f4ff4145b7abb60fd9bec168ae4b60f23b4. * fixes * fixes * Removed privacy keycard calls * Revert "Removed privacy keycard calls" This reverts commit d70ef505a1f40b87159099761f5fce5a31e3f17b. * Add domain separators * Removed privacy txs for keycard * CI fixes * CI fixes * addressed some comments * fix ci * initialize branch * ci fixes * fix integration test issue and updated keycard firmware * addressed more comments * fixed deny * remove keycard-py * fixed from earlier merge * add hash_message tests * add test * fix deny * CI fixes * fixed integration tests * Update public.rs * update artifacts * privacy command fixes * ci and comments * addressed comments * comment fixes * fixes from merging main * adding support to other programs * expanded support * ci fixes * ci and add private account keys test * some fixes and setup notes * Ci fixes * ci fixes * update key paths to avoid collisions in tests * added separated files for keycard_tests_2.sh * first round of comments * Revert "Merge branch 'main' into marvin/keycard-commands" This reverts commit 3fce53f663a3996938dddf77680854570063ca21, reversing changes made to e7b42a5177641455a8917bd2e29db20afd9690e5. * python comments * addressed comments * compile error fixed * fix artifacts * fix main merge error * adjust signer logic workflow * updating logic * fmt * refactored * clippy fix * minor fix * addressing comments * minor fix * ci fix * addressed deferred comments * clean up * minor cleanup * ci fixes * fmt fix * feat!(wallet): Merged `SigningGroup` with `AccountManager` (#500) * feat: account manager extension * feat(wallet): added unified way of sending public transactions to all facades * fix(wallet): no sign option added * fix(deny): deny fix * fix(wallet): suggestion 1 * fix(wallet): suggestion fix 1 * feat!: Add new path for externally provided seed to the circuit. BREAKING CHANGE: add identity variants to the circuit and change semantics for `Claim::Authorized` for private PDAs * feat(ci): use separate job per each integration tests module * feat(ci): cache rust artifacts * feat(ci): build integration tests binary once and reuse it * fix(wallet): fmt * ci: add bench-regression workflow with criterion-compare for crypto_primitives_bench * fix(wallet): merge postfix * feat!(wallet): SigningGroup merged with AccountManager * fix(ci): deny and artifacts fix * fix(deny): deny fix * fix keycard and lint --------- Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com> Co-authored-by: Daniil Polyakov <arjentix@gmail.com> Co-authored-by: Moudy <m.ellaz@hotmail.com> Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Co-authored-by: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> * addressed comments * minor comments * Rebase to main * CI fixes --------- Co-authored-by: Pravdyvy <46261001+Pravdyvy@users.noreply.github.com> Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com> Co-authored-by: Daniil Polyakov <arjentix@gmail.com> Co-authored-by: Moudy <m.ellaz@hotmail.com> Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com>
590 lines
22 KiB
Rust
590 lines
22 KiB
Rust
use anyhow::{Context as _, Result};
|
|
use clap::Subcommand;
|
|
use itertools::Itertools as _;
|
|
use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex};
|
|
use lee::{Account, PublicKey, program::Program};
|
|
use lee_core::Identifier;
|
|
use token_core::{TokenDefinition, TokenHolding};
|
|
|
|
use crate::{
|
|
WalletCore,
|
|
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
|
|
cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand},
|
|
};
|
|
|
|
/// Represents generic chain CLI subcommand.
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
pub enum AccountSubcommand {
|
|
/// Resolve an account mention and print just the account ID (no privacy prefix).
|
|
Id {
|
|
/// Account id with privacy prefix, label, or BIP-32 key path.
|
|
#[arg(long)]
|
|
account_id: CliAccountMention,
|
|
},
|
|
/// Get account data.
|
|
Get {
|
|
/// Flag to get raw account data.
|
|
#[arg(short, long)]
|
|
raw: bool,
|
|
/// Display keys (pk for public accounts, npk/vpk for private accounts).
|
|
#[arg(short, long)]
|
|
keys: bool,
|
|
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
|
#[arg(short, long)]
|
|
account_id: CliAccountMention,
|
|
},
|
|
/// Produce new public or private account.
|
|
#[command(subcommand)]
|
|
New(NewSubcommand),
|
|
/// Sync private accounts.
|
|
SyncPrivate,
|
|
/// List all accounts owned by the wallet.
|
|
#[command(visible_alias = "ls")]
|
|
List {
|
|
/// Show detailed account information (like `account get`).
|
|
#[arg(short, long)]
|
|
long: bool,
|
|
},
|
|
/// Set a label for an account.
|
|
Label {
|
|
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
|
#[arg(short, long)]
|
|
account_id: CliAccountMention,
|
|
/// The label to assign to the account.
|
|
#[arg(short, long)]
|
|
label: Label,
|
|
},
|
|
/// Import external account.
|
|
#[command(subcommand)]
|
|
Import(ImportSubcommand),
|
|
/// Print the npk and vpk for a private account, one per line.
|
|
///
|
|
/// Outputs two lines: npk (hex) then vpk (hex). Save to a file and share it
|
|
/// with senders so they can reference it with `--to-keys /path/to/file`.
|
|
///
|
|
/// ```text
|
|
/// wallet account show-keys --account-id Private/... > alice.keys
|
|
/// ```
|
|
#[command(name = "show-keys")]
|
|
ShowKeys {
|
|
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
|
#[arg(long)]
|
|
account_id: CliAccountMention,
|
|
},
|
|
}
|
|
|
|
/// Represents generic register CLI subcommand.
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
pub enum NewSubcommand {
|
|
/// Register new public account.
|
|
Public {
|
|
#[arg(long)]
|
|
/// Chain index of a parent node.
|
|
cci: Option<ChainIndex>,
|
|
#[arg(short, long)]
|
|
/// Label to assign to the new account.
|
|
label: Option<Label>,
|
|
},
|
|
/// Single-account convenience: creates a key node and auto-registers one account with a random
|
|
/// identifier.
|
|
Private {
|
|
#[arg(long)]
|
|
/// Chain index of a parent node.
|
|
cci: Option<ChainIndex>,
|
|
#[arg(short, long)]
|
|
/// Label to assign to the new account.
|
|
label: Option<Label>,
|
|
},
|
|
/// Create a shared private account from a group's GMS.
|
|
PrivateGms {
|
|
/// Group name to derive keys from.
|
|
group: Label,
|
|
#[arg(short, long)]
|
|
/// Label to assign to the new account.
|
|
label: Option<Label>,
|
|
#[arg(long)]
|
|
/// Create a PDA account (requires --seed and --program-id).
|
|
pda: bool,
|
|
#[arg(long, requires = "pda")]
|
|
/// PDA seed as 64-character hex string.
|
|
seed: Option<String>,
|
|
#[arg(long, requires = "pda")]
|
|
/// Program ID as hex string.
|
|
program_id: Option<String>,
|
|
#[arg(long, requires = "pda")]
|
|
/// Identifier that diversifies this PDA within the (`program_id`, seed, npk) family.
|
|
/// Defaults to a random value if not specified.
|
|
identifier: Option<u128>,
|
|
},
|
|
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
|
|
/// registering any account.
|
|
PrivateAccountsKey {
|
|
#[arg(long)]
|
|
/// Chain index of a parent node.
|
|
cci: Option<ChainIndex>,
|
|
},
|
|
}
|
|
|
|
impl WalletSubcommand for NewSubcommand {
|
|
async fn handle_subcommand(
|
|
self,
|
|
wallet_core: &mut WalletCore,
|
|
) -> Result<SubcommandReturnValue> {
|
|
match self {
|
|
Self::Public { cci, label } => {
|
|
if let Some(label) = &label {
|
|
wallet_core.storage().check_label_availability(label)?;
|
|
}
|
|
|
|
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
|
|
|
|
let private_key = wallet_core
|
|
.storage
|
|
.key_chain()
|
|
.pub_account_signing_key(account_id)
|
|
.unwrap();
|
|
|
|
let public_key = PublicKey::new_from_private_key(private_key);
|
|
|
|
if let Some(label) = label {
|
|
wallet_core
|
|
.storage_mut()
|
|
.add_label(label, AccountIdWithPrivacy::Public(account_id))?;
|
|
}
|
|
|
|
println!(
|
|
"Generated new account with account_id Public/{account_id} at path {chain_index}"
|
|
);
|
|
println!("With pk {}", hex::encode(public_key.value()));
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
|
}
|
|
Self::Private { cci, label } => {
|
|
if let Some(label) = &label {
|
|
wallet_core.storage().check_label_availability(label)?;
|
|
}
|
|
|
|
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
|
|
|
|
if let Some(label) = label {
|
|
wallet_core
|
|
.storage_mut()
|
|
.add_label(label, AccountIdWithPrivacy::Private(account_id))?;
|
|
}
|
|
|
|
let found_acc = wallet_core
|
|
.storage()
|
|
.key_chain()
|
|
.private_account(account_id)
|
|
.expect("Account should exist after creation");
|
|
let key_chain = found_acc.key_chain;
|
|
|
|
println!(
|
|
"Generated new account with account_id Private/{account_id} at path {chain_index}"
|
|
);
|
|
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
|
|
println!(
|
|
"With vpk {}",
|
|
hex::encode(key_chain.viewing_public_key.to_bytes())
|
|
);
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
|
}
|
|
Self::PrivateGms {
|
|
group,
|
|
label,
|
|
pda,
|
|
seed,
|
|
program_id,
|
|
identifier,
|
|
} => {
|
|
if let Some(label) = &label {
|
|
wallet_core.storage().check_label_availability(label)?;
|
|
}
|
|
|
|
let info = if pda {
|
|
let seed_hex = seed.context("--seed is required for PDA accounts")?;
|
|
let pid_hex =
|
|
program_id.context("--program-id is required for PDA accounts")?;
|
|
|
|
let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
|
|
.context("Invalid seed hex")?
|
|
.try_into()
|
|
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
|
|
let pda_seed = lee_core::program::PdaSeed::new(seed_bytes);
|
|
|
|
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
|
|
if pid_bytes.len() != 32 {
|
|
anyhow::bail!("Program ID must be exactly 32 bytes");
|
|
}
|
|
let mut pid: lee_core::program::ProgramId = [0; 8];
|
|
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
|
|
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
|
|
}
|
|
|
|
wallet_core.create_shared_pda_account(
|
|
group.clone(),
|
|
pda_seed,
|
|
pid,
|
|
identifier.unwrap_or_else(rand::random),
|
|
)?
|
|
} else {
|
|
wallet_core.create_shared_regular_account(group.clone())?
|
|
};
|
|
|
|
if let Some(label) = label {
|
|
wallet_core
|
|
.storage_mut()
|
|
.add_label(label, AccountIdWithPrivacy::Private(info.account_id))?;
|
|
}
|
|
|
|
println!("Shared account from group '{group}'");
|
|
println!("AccountId: Private/{}", info.account_id);
|
|
println!("NPK: {}", hex::encode(info.npk.0));
|
|
println!("VPK: {}", hex::encode(info.vpk.to_bytes()));
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
Ok(SubcommandReturnValue::RegisterAccount {
|
|
account_id: info.account_id,
|
|
})
|
|
}
|
|
Self::PrivateAccountsKey { cci } => {
|
|
let chain_index = wallet_core.create_private_accounts_key(cci);
|
|
let key_chain = wallet_core
|
|
.storage()
|
|
.key_chain()
|
|
.private_account_key_chain_by_index(&chain_index)
|
|
.expect("Key chain should exist after creation");
|
|
|
|
println!("Generated new private key node at path {chain_index}");
|
|
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
|
|
println!(
|
|
"With vpk {}",
|
|
hex::encode(key_chain.viewing_public_key.to_bytes())
|
|
);
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WalletSubcommand for AccountSubcommand {
|
|
async fn handle_subcommand(
|
|
self,
|
|
wallet_core: &mut WalletCore,
|
|
) -> Result<SubcommandReturnValue> {
|
|
match self {
|
|
Self::Id { account_id } => {
|
|
let resolved = account_id.resolve(wallet_core.storage())?;
|
|
let id = match resolved {
|
|
AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id,
|
|
};
|
|
println!("{id}");
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
Self::Get {
|
|
raw,
|
|
keys,
|
|
account_id,
|
|
} => {
|
|
let resolved = account_id.resolve(wallet_core.storage())?;
|
|
wallet_core
|
|
.storage()
|
|
.labels_for_account(resolved)
|
|
.for_each(|label| {
|
|
println!("Label: {label}");
|
|
});
|
|
|
|
let account = wallet_core.get_account(resolved).await?;
|
|
|
|
// Helper closure to display keys for the account
|
|
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
|
|
match resolved {
|
|
AccountIdWithPrivacy::Public(account_id) => {
|
|
let private_key = wallet_core
|
|
.storage
|
|
.key_chain()
|
|
.pub_account_signing_key(account_id)
|
|
.context("Public account not found in storage")?;
|
|
|
|
let public_key = PublicKey::new_from_private_key(private_key);
|
|
println!("pk {}", hex::encode(public_key.value()));
|
|
}
|
|
AccountIdWithPrivacy::Private(account_id) => {
|
|
let acc = wallet_core
|
|
.storage
|
|
.key_chain()
|
|
.private_account(account_id)
|
|
.context("Private account not found in storage")?;
|
|
|
|
println!("npk {}", hex::encode(acc.key_chain.nullifier_public_key.0));
|
|
println!(
|
|
"vpk {}",
|
|
hex::encode(acc.key_chain.viewing_public_key.to_bytes())
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
};
|
|
|
|
if account == Account::default() {
|
|
println!("Account is Uninitialized");
|
|
|
|
if keys {
|
|
display_keys(wallet_core)?;
|
|
}
|
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
}
|
|
|
|
if raw {
|
|
let account_hr: HumanReadableAccount = account.into();
|
|
println!("{account_hr}");
|
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
}
|
|
|
|
let (description, json_view) = format_account_details(&account);
|
|
println!("{description}");
|
|
println!("{json_view}");
|
|
|
|
if keys {
|
|
display_keys(wallet_core)?;
|
|
}
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
Self::New(new_subcommand) => new_subcommand.handle_subcommand(wallet_core).await,
|
|
Self::SyncPrivate => {
|
|
let curr_last_block = wallet_core.sync_to_latest_block().await?;
|
|
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
|
|
}
|
|
Self::List { long } => {
|
|
let key_chain = &wallet_core.storage.key_chain();
|
|
let storage = wallet_core.storage();
|
|
|
|
let format_with_label =
|
|
|id: AccountIdWithPrivacy, chain_index: Option<&ChainIndex>| {
|
|
let id_str =
|
|
chain_index.map_or_else(|| id.to_string(), |cci| format!("{cci} {id}"));
|
|
|
|
let labels = storage.labels_for_account(id).format(", ").to_string();
|
|
if labels.is_empty() {
|
|
id_str
|
|
} else {
|
|
format!("{id_str} [{labels}]")
|
|
}
|
|
};
|
|
|
|
if !long {
|
|
let accounts = key_chain
|
|
.account_ids()
|
|
.map(|(id, idx)| format_with_label(id, idx))
|
|
.format("\n");
|
|
println!("{accounts}");
|
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
}
|
|
|
|
// Detailed listing with --long flag
|
|
|
|
// Public key tree accounts
|
|
for (id, chain_index) in key_chain.public_account_ids() {
|
|
println!(
|
|
"{}",
|
|
format_with_label(AccountIdWithPrivacy::Public(id), chain_index)
|
|
);
|
|
match wallet_core.get_account_public(id).await {
|
|
Ok(account) if account != Account::default() => {
|
|
let (description, json_view) = format_account_details(&account);
|
|
println!(" {description}");
|
|
println!(" {json_view}");
|
|
}
|
|
Ok(_) => println!(" Uninitialized"),
|
|
Err(e) => println!(" Error fetching account: {e}"),
|
|
}
|
|
}
|
|
|
|
// Private key tree accounts
|
|
for (id, chain_index) in key_chain.private_account_ids() {
|
|
println!(
|
|
"{}",
|
|
format_with_label(AccountIdWithPrivacy::Private(id), chain_index)
|
|
);
|
|
match wallet_core.get_account_private(id) {
|
|
Some(account) if account != Account::default() => {
|
|
let (description, json_view) = format_account_details(&account);
|
|
println!(" {description}");
|
|
println!(" {json_view}");
|
|
}
|
|
Some(_) => println!(" Uninitialized"),
|
|
None => println!(" Not found in local storage"),
|
|
}
|
|
}
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
Self::Label { account_id, label } => {
|
|
let account_id = account_id.resolve(wallet_core.storage())?;
|
|
|
|
wallet_core
|
|
.storage_mut()
|
|
.add_label(label.clone(), account_id)?;
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
println!("Label '{label}' set for account {account_id}");
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
Self::Import(import_subcommand) => {
|
|
import_subcommand.handle_subcommand(wallet_core).await
|
|
}
|
|
Self::ShowKeys { account_id } => {
|
|
let resolved = account_id.resolve(wallet_core.storage())?;
|
|
let AccountIdWithPrivacy::Private(account_id) = resolved else {
|
|
anyhow::bail!(
|
|
"wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts"
|
|
);
|
|
};
|
|
let entry = wallet_core
|
|
.storage()
|
|
.key_chain()
|
|
.private_account(account_id)
|
|
.ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?;
|
|
println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0));
|
|
println!(
|
|
"{}",
|
|
hex::encode(entry.key_chain.viewing_public_key.to_bytes())
|
|
);
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
pub enum ImportSubcommand {
|
|
/// Import a public account signing key.
|
|
Public {
|
|
/// Private key in hex format.
|
|
#[arg(long)]
|
|
private_key: lee::PrivateKey,
|
|
},
|
|
/// Import a private account keychain and account state.
|
|
Private {
|
|
/// Private account keychain JSON.
|
|
#[arg(long)]
|
|
key_chain_json: String,
|
|
/// Private account state JSON (`HumanReadableAccount`).
|
|
#[arg(long)]
|
|
account_state: HumanReadableAccount,
|
|
/// Chain index.
|
|
#[arg(long)]
|
|
chain_index: Option<ChainIndex>,
|
|
/// Identifier.
|
|
#[arg(long, default_value = "0")]
|
|
identifier: Identifier,
|
|
},
|
|
}
|
|
|
|
impl WalletSubcommand for ImportSubcommand {
|
|
async fn handle_subcommand(
|
|
self,
|
|
wallet_core: &mut WalletCore,
|
|
) -> Result<SubcommandReturnValue> {
|
|
match self {
|
|
Self::Public { private_key } => {
|
|
let account_id =
|
|
lee::AccountId::from(&lee::PublicKey::new_from_private_key(&private_key));
|
|
|
|
wallet_core
|
|
.storage_mut()
|
|
.key_chain_mut()
|
|
.add_imported_public_account(private_key);
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
println!("Imported public account Public/{account_id}");
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
Self::Private {
|
|
key_chain_json,
|
|
account_state,
|
|
chain_index,
|
|
identifier,
|
|
} => {
|
|
let key_chain: KeyChain = serde_json::from_str(&key_chain_json)
|
|
.map_err(|err| anyhow::anyhow!("Invalid key chain JSON: {err}"))?;
|
|
let account = lee::Account::from(account_state);
|
|
let account_id =
|
|
lee::AccountId::from((&key_chain.nullifier_public_key, identifier));
|
|
|
|
wallet_core
|
|
.storage_mut()
|
|
.key_chain_mut()
|
|
.add_imported_private_account(key_chain, chain_index, identifier, account);
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
println!("Imported private account Private/{account_id}");
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Formats account details for display, returning (description, `json_view`).
|
|
fn format_account_details(account: &Account) -> (String, String) {
|
|
let auth_tr_prog_id = Program::authenticated_transfer_program().id();
|
|
let token_prog_id = Program::token().id();
|
|
|
|
match &account.program_owner {
|
|
o if *o == auth_tr_prog_id => {
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
(
|
|
"Account owned by authenticated transfer program".to_owned(),
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
)
|
|
}
|
|
o if *o == token_prog_id => TokenDefinition::try_from(&account.data)
|
|
.map(|token_def| {
|
|
(
|
|
"Definition account owned by token program".to_owned(),
|
|
serde_json::to_string(&token_def).unwrap(),
|
|
)
|
|
})
|
|
.or_else(|_| {
|
|
TokenHolding::try_from(&account.data).map(|token_hold| {
|
|
(
|
|
"Holding account owned by token program".to_owned(),
|
|
serde_json::to_string(&token_hold).unwrap(),
|
|
)
|
|
})
|
|
})
|
|
.unwrap_or_else(|_| {
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
(
|
|
"Unknown token program account".to_owned(),
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
)
|
|
}),
|
|
_ => {
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
(
|
|
"Account".to_owned(),
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
)
|
|
}
|
|
}
|
|
}
|