diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index 191db9001..00a63a90e 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -23,7 +23,8 @@ use crate::{ programs::{ amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, bridge::BridgeSubcommand, native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, - token::TokenProgramAgnosticSubcommand, vault::VaultSubcommand, + system_program::SystemSubcommand, token::TokenProgramAgnosticSubcommand, + vault::VaultSubcommand, }, statistics::StatisticsSubcommand, }, @@ -73,6 +74,9 @@ pub enum Command { /// Vault program interaction subcommand. #[command(subcommand)] Vault(VaultSubcommand), + /// System Program interaction subcommand (e.g. reclaim an account). + #[command(subcommand)] + System(SystemSubcommand), /// Bridge program interaction subcommand. #[command(subcommand)] Bridge(BridgeSubcommand), @@ -218,6 +222,10 @@ impl Default for CliAccountMention { } } +#[expect( + clippy::cognitive_complexity, + reason = "flat dispatch over every CLI command" +)] pub async fn execute_subcommand( wallet_core: &mut WalletCore, command: Command, @@ -235,6 +243,9 @@ pub async fn execute_subcommand( Command::Pinata(pinata_subcommand) => { pinata_subcommand.handle_subcommand(wallet_core).await? } + Command::System(system_subcommand) => { + system_subcommand.handle_subcommand(wallet_core).await? + } Command::CheckHealth => { let remote_program_ids = wallet_core .get_program_ids() diff --git a/lez/wallet/src/cli/programs/mod.rs b/lez/wallet/src/cli/programs/mod.rs index bc8abaf2b..3239bd134 100644 --- a/lez/wallet/src/cli/programs/mod.rs +++ b/lez/wallet/src/cli/programs/mod.rs @@ -3,5 +3,6 @@ pub mod ata; pub mod bridge; pub mod native_token_transfer; pub mod pinata; +pub mod system_program; pub mod token; pub mod vault; diff --git a/lez/wallet/src/cli/programs/system_program.rs b/lez/wallet/src/cli/programs/system_program.rs new file mode 100644 index 000000000..ea39f5065 --- /dev/null +++ b/lez/wallet/src/cli/programs/system_program.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use clap::Subcommand; +use lee::AccountId; + +use crate::{ + WalletCore, + account::AccountIdWithPrivacy, + cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand}, + program_facades::system_program::SystemProgram, +}; + +/// Represents generic CLI subcommand for a wallet working with the System Program. +#[derive(Subcommand, Debug, Clone)] +pub enum SystemSubcommand { + /// !!!WARNING!!! Reclaim an account: wipe all of its data and hand it to a new owner, + /// bypassing the program that currently owns it. Balance is preserved. + /// + /// The account authorizes its own reset. Only public accounts are supported. + Reclaim { + /// Account to reclaim. Must have authorization. + #[arg(long)] + account_id: CliAccountMention, + /// New owner - valid 32 byte base58 string WITHOUT privacy prefix. Defaults to the + /// authenticated transfer program, which keeps the balance spendable. + #[arg(long)] + new_owner: Option, + }, +} + +impl WalletSubcommand for SystemSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + Self::Reclaim { + account_id, + new_owner, + } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Public(pub_account_id) = resolved else { + anyhow::bail!( + "Shielded reclaim is not yet supported; only public accounts can be reclaimed" + ); + }; + let new_owner = Some( + new_owner.unwrap_or_else(|| programs::authenticated_transfer().id().into()), + ); + + let tx_hash = SystemProgram(wallet_core) + .clear( + account_id.into_public_identity(pub_account_id, true), + new_owner, + ) + .await?; + + wallet_core + .poll_and_finalize_public_transaction(tx_hash) + .await + } + } + } +} diff --git a/lez/wallet/src/program_facades/mod.rs b/lez/wallet/src/program_facades/mod.rs index a634e4ca9..22d1258a2 100644 --- a/lez/wallet/src/program_facades/mod.rs +++ b/lez/wallet/src/program_facades/mod.rs @@ -6,5 +6,6 @@ pub mod ata; pub mod bridge; pub mod native_token_transfer; pub mod pinata; +pub mod system_program; pub mod token; pub mod vault; diff --git a/lez/wallet/src/program_facades/system_program.rs b/lez/wallet/src/program_facades/system_program.rs new file mode 100644 index 000000000..b3884ed5a --- /dev/null +++ b/lez/wallet/src/program_facades/system_program.rs @@ -0,0 +1,23 @@ +use common::HashType; +use lee::{AccountId, program::Program}; +use lee_core::program::{DEFAULT_PROGRAM_ID, SystemInstruction}; + +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore}; + +pub struct SystemProgram<'wallet>(pub &'wallet WalletCore); + +impl SystemProgram<'_> { + pub async fn clear( + &self, + account: AccountIdentity, + new_owner: Option, + ) -> Result { + let instruction = SystemInstruction::Clear { new_owner }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + self.0 + .send_pub_tx(vec![account], instruction_data, DEFAULT_PROGRAM_ID) + .await + } +}