use base64::Engine;
use thru_base::txn_tools::SYSTEM_PROGRAM;
use std::time::Duration;
use thru_base::rpc_types::{MakeStateProofConfig, ProofType};
use thru_base::tn_account::{TN_ACCOUNT_META_FOOTPRINT, TnAccountMeta};
use thru_base::{StateProof, TransactionBuilder};
use crate::cli::AccountCommands;
use crate::config::Config;
use crate::crypto::keypair_from_hex;
use crate::error::CliError;
use crate::output;
use crate::utils::format_vm_error;
use thru_client::{Client, ClientBuilder};
fn resolve_fee_payer_and_target(
config: &Config,
fee_payer: Option<&str>,
target_account: &str,
) -> Result<(thru_base::tn_tools::KeyPair, thru_base::tn_tools::Pubkey), CliError> {
let _fee_payer_pubkey = crate::commands::rpc::resolve_account_input(fee_payer, config)?;
let fee_payer_private_key = if let Some(fee_payer_name) = fee_payer {
config.keys.get_key(fee_payer_name).map_err(|_| {
CliError::Validation(format!(
"Fee payer key '{}' not found in configuration",
fee_payer_name
))
})?
} else {
config.keys.get_default_key()?
};
let fee_payer_keypair = keypair_from_hex(fee_payer_private_key)?;
let target_pubkey = crate::commands::rpc::resolve_account_input(Some(target_account), config)?;
Ok((fee_payer_keypair, target_pubkey))
}
pub async fn handle_account_command(
config: &Config,
subcommand: AccountCommands,
json_format: bool,
) -> Result<(), CliError> {
match subcommand {
AccountCommands::Create { key_name } => {
create_account(config, key_name.as_deref(), json_format).await
}
AccountCommands::Info { key_name } => {
get_account_info(config, key_name.as_deref(), json_format).await
}
AccountCommands::Transactions {
account,
page_size,
page_token,
} => {
list_account_transactions(
config,
account.as_deref(),
page_size,
page_token,
json_format,
)
.await
}
AccountCommands::Compress {
target_account,
fee_payer,
} => compress_account(config, fee_payer.as_deref(), &target_account, json_format).await,
AccountCommands::Decompress {
target_account,
fee_payer,
} => decompress_account(config, fee_payer.as_deref(), &target_account, json_format).await,
AccountCommands::PrepareDecompression { account } => {
prepare_account_decompression(config, &account, json_format).await
}
}
}
async fn list_account_transactions(
config: &Config,
account_input: Option<&str>,
page_size: Option<u32>,
page_token: Option<String>,
json_format: bool,
) -> Result<(), CliError> {
let account_pubkey = crate::commands::rpc::resolve_account_input(account_input, config)?;
let client = create_rpc_client(config)?;
let page = client
.list_transactions_for_account(&account_pubkey, page_size, page_token)
.await?;
let account_str = account_pubkey.to_string();
let next_page_token = page.next_page_token.clone();
let signatures: Vec<String> = page.signatures.iter().map(|sig| sig.to_string()).collect();
let response =
output::create_account_transactions_response(&account_str, signatures, next_page_token);
output::print_output(response, json_format);
Ok(())
}
async fn create_account(
config: &Config,
key_name: Option<&str>,
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info("Creating account with fee payer proof...");
}
let key_name = key_name.unwrap_or("default");
let private_key_hex = config.keys.get_key(key_name).map_err(|_| {
CliError::Validation(format!("Key '{}' not found in configuration", key_name))
})?;
let keypair = keypair_from_hex(private_key_hex)?;
let account_pubkey = &keypair.address_string;
if !json_format {
output::print_info(&format!("Account public key: {}", account_pubkey));
}
let client = create_rpc_client(config)?;
let block_height = client.get_block_height().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get current slot: {}", e))
})?;
let chain_info = client.get_chain_info().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get chain info: {}", e))
})?;
let current_slot = block_height.finalized_height;
if !json_format {
output::print_info(&format!("Using slot: {}", current_slot));
}
let state_proof_config = MakeStateProofConfig {
proof_type: ProofType::Creating,
slot: None,
};
if !json_format {
output::print_info("Calling makeStateProof RPC method...");
}
let state_proof_bytes = client
.make_state_proof(account_pubkey, &state_proof_config)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to create state proof: {}", e))
})?;
let state_proof = StateProof::from_wire(&state_proof_bytes).ok_or_else(|| {
CliError::TransactionSubmission("Failed to parse state proof response".to_string())
})?;
if !json_format {
output::print_success("State proof created successfully");
output::print_info("Building transaction with fee payer proof...");
}
let transaction = TransactionBuilder::build_create_with_fee_payer_proof(
keypair.public_key,
current_slot,
&state_proof,
)
.map_err(|e| CliError::TransactionSubmission(format!("Failed to build transaction: {}", e)))?;
if !json_format {
output::print_info("Signing transaction...");
}
let mut transaction = transaction.with_chain_id(chain_info.chain_id);
transaction.sign(&keypair.private_key).map_err(|e| {
CliError::TransactionSubmission(format!("Failed to sign transaction: {}", e))
})?;
if !json_format {
output::print_info("Submitting transaction...");
}
let transaction_bytes = transaction.to_wire();
let timeout = Duration::from_secs(config.timeout_seconds);
let transaction_details = client
.execute_transaction(&transaction_bytes, timeout)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to submit transaction: {}", e))
})?;
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, _) = vm_error_strings(transaction_details.vm_error);
let error_msg = format!(
"Transaction failed with execution result: {}{}",
transaction_details.execution_result as i64, vm_error_msg
);
let response = output::create_account_create_response(
key_name,
account_pubkey.as_str(),
transaction_details.signature.as_str(),
"failed",
);
output::print_output(response, json_format);
return Err(CliError::TransactionSubmission(error_msg));
}
let response = output::create_account_create_response(
key_name,
account_pubkey.as_str(),
transaction_details.signature.as_str(),
"success",
);
output::print_output(response, json_format);
if !json_format {
output::print_success(&format!(
"Account creation transaction completed. Signature: {}",
transaction_details.signature.as_str()
));
}
Ok(())
}
async fn get_account_info(
config: &Config,
key_name: Option<&str>,
json_format: bool,
) -> Result<(), CliError> {
crate::commands::rpc::get_account_info(config, key_name, None, None, json_format).await
}
async fn compress_account(
config: &Config,
fee_payer: Option<&str>,
target_account: &str,
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info("Compressing account...");
}
let (fee_payer_keypair, target_pubkey) =
resolve_fee_payer_and_target(config, fee_payer, target_account)?;
if !json_format {
let fee_payer_name = fee_payer.unwrap_or("default");
output::print_info(&format!(
"Fee payer: {} ({})",
fee_payer_name,
fee_payer_keypair.address_string.as_str()
));
output::print_info(&format!("Target account: {}", target_pubkey.as_str()));
}
let client = create_rpc_client(config)?;
let block_height = client.get_block_height().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get current block height: {}", e))
})?;
let chain_info = client.get_chain_info().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get chain info: {}", e))
})?;
let start_slot = block_height.executed_height + 1;
if !json_format {
output::print_info(&format!("Using start slot: {}", start_slot));
}
let account_info = client
.get_account_info(&target_pubkey, None, None)
.await
.map_err(|e| CliError::TransactionSubmission(format!("Failed to get account info: {}", e)))?
.ok_or_else(|| {
CliError::Validation(format!(
"Target account {} does not exist or is already compressed",
target_pubkey.as_str()
))
})?;
let state_proof_config = MakeStateProofConfig {
proof_type: if account_info.is_new {
ProofType::Creating
} else {
ProofType::Updating
},
slot: None,
};
if !json_format {
output::print_info("Creating state proof for target account...");
}
let state_proof_bytes = client
.make_state_proof(&target_pubkey, &state_proof_config)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to create state proof: {}", e))
})?;
if !json_format {
output::print_success("State proof created successfully");
output::print_info("Building compression transaction...");
}
let account_info = client
.get_account_info(&target_pubkey, None, None)
.await
.map_err(|e| CliError::TransactionSubmission(format!("Failed to get account info: {}", e)))?
.ok_or_else(|| {
CliError::Validation(format!(
"Target account {} does not exist or is already compressed",
target_pubkey.as_str()
))
})?;
let account_size = account_info.data_size as u32;
if !json_format {
output::print_info(&format!("Account size: {} bytes", account_size));
}
let fee_payer_account_info = client
.get_account_info(&fee_payer_keypair.address_string, None, None)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get fee payer account info: {}", e))
})?
.ok_or_else(|| {
CliError::Validation(format!(
"Fee payer account {} not found. Please ensure the account is funded.",
fee_payer_keypair.address_string.as_str()
))
})?;
let nonce = fee_payer_account_info.nonce;
if !json_format {
output::print_info(&format!("Fee payer nonce: {}", nonce));
}
let mut transaction = TransactionBuilder::build_compress_account(
fee_payer_keypair.public_key, SYSTEM_PROGRAM, target_pubkey.to_bytes()?, &state_proof_bytes, 1, nonce, start_slot, account_size, )
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to build compression transaction: {}", e))
})?;
transaction = transaction
.with_compute_units(100_300 + account_size * 2)
.with_state_units(10_000)
.with_memory_units(10_000)
.with_expiry_after(100)
.with_chain_id(chain_info.chain_id);
if !json_format {
output::print_info("Signing transaction...");
}
transaction
.sign(&fee_payer_keypair.private_key)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to sign transaction: {}", e))
})?;
if !json_format {
output::print_info("Executing transaction...");
}
let transaction_bytes = transaction.to_wire();
let timeout = Duration::from_secs(config.timeout_seconds);
let transaction_details = client
.execute_transaction(&transaction_bytes, timeout)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to execute transaction: {}", e))
})?;
if !json_format {
output::print_success(&format!(
"Transaction completed: {}",
transaction_details.signature.as_str()
));
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, vm_error_label) = vm_error_strings(transaction_details.vm_error);
output::print_warning(&format!(
"Transaction completed with execution result: {} vm_error: {}{}",
transaction_details.execution_result as i64, vm_error_label, vm_error_msg
));
}
}
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, vm_error_label) = vm_error_strings(transaction_details.vm_error);
return Err(CliError::TransactionSubmission(format!(
"Transaction failed with execution result: {} (VM error: {}{}, User error: {})",
transaction_details.execution_result as i64,
vm_error_label,
vm_error_msg,
transaction_details.user_error_code
)));
}
if json_format {
let response = serde_json::json!({
"account_compress": {
"fee_payer": fee_payer_keypair.address_string.as_str(),
"target_account": target_pubkey.as_str(),
"signature": transaction_details.signature.as_str(),
"status": "success",
"execution_result": transaction_details.execution_result,
"vm_error": transaction_details.vm_error,
"vm_error_name": vm_error_strings(transaction_details.vm_error).1,
"user_error_code": transaction_details.user_error_code,
"compute_units_consumed": transaction_details.compute_units_consumed,
"slot": transaction_details.slot
}
});
output::print_output(response, true);
} else {
output::print_success("Account compression completed successfully");
println!("Fee payer: {}", fee_payer_keypair.address_string.as_str());
println!("Target account: {}", target_pubkey.as_str());
println!("Signature: {}", transaction_details.signature.as_str());
println!("Slot: {}", transaction_details.slot);
println!(
"Compute units consumed: {}",
transaction_details.compute_units_consumed
);
}
Ok(())
}
async fn decompress_account(
config: &Config,
fee_payer: Option<&str>,
target_account: &str,
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info("Decompressing account...");
}
if target_account == "default" {
let error_msg =
"Cannot decompress 'default' account. Please specify a different target account.";
if json_format {
let response = serde_json::json!({
"error": error_msg
});
output::print_output(response, true);
} else {
output::print_error(error_msg);
}
return Err(CliError::Validation(error_msg.to_string()));
}
let (fee_payer_keypair, target_pubkey) =
resolve_fee_payer_and_target(config, fee_payer, target_account)?;
if !json_format {
let fee_payer_name = fee_payer.unwrap_or("default");
output::print_info(&format!(
"Fee payer: {} ({})",
fee_payer_name,
fee_payer_keypair.address_string.as_str()
));
output::print_info(&format!("Target account: {}", target_pubkey.as_str()));
}
let client = create_rpc_client(config)?;
if !json_format {
output::print_info("Preparing account decompression...");
}
let decomp_response = client
.prepare_account_decompression(&target_pubkey)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!(
"Failed to prepare account decompression: {}",
e
))
})?;
let decomp_data = base64::engine::general_purpose::STANDARD
.decode(&decomp_response.account_data)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to decode decompression data: {}", e))
})?;
let state_proof_bytes = base64::engine::general_purpose::STANDARD
.decode(&decomp_response.state_proof)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to decode state proof: {}", e))
})?;
if !json_format {
output::print_info(&format!(
"Decompression data size: {} bytes",
decomp_data.len()
));
output::print_info(&format!(
"State proof size: {} bytes",
state_proof_bytes.len()
));
}
if decomp_data.len() < TN_ACCOUNT_META_FOOTPRINT {
return Err(CliError::Validation(
"Decompression data is too small to contain metadata".to_string(),
));
}
let block_height = client.get_block_height().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get current block height: {}", e))
})?;
let chain_info = client.get_chain_info().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get chain info: {}", e))
})?;
let start_slot = block_height.executed_height + 1;
let fee_payer_account_info = client
.get_account_info(&fee_payer_keypair.address_string, None, None)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get fee payer account info: {}", e))
})?
.ok_or_else(|| {
CliError::Validation(format!(
"Fee payer account {} not found. Please ensure the account is funded.",
fee_payer_keypair.address_string.as_str()
))
})?;
let nonce = fee_payer_account_info.nonce;
const MAX_TRANSACTION_SIZE: usize = 32_768;
const TRANSACTION_OVERHEAD: usize = 1024; const MAX_DATA_IN_TRANSACTION: usize = MAX_TRANSACTION_SIZE - TRANSACTION_OVERHEAD;
const MAX_UPLOADER_SIZE: usize = 16 * 1024 * 1024;
if decomp_data.len() <= MAX_DATA_IN_TRANSACTION {
decompress_direct(
config,
&fee_payer_keypair,
&target_pubkey,
&decomp_data,
&state_proof_bytes,
nonce,
start_slot,
chain_info.chain_id,
json_format,
)
.await
} else if decomp_data.len() <= MAX_UPLOADER_SIZE {
decompress_with_uploader(
config,
&fee_payer_keypair,
&target_pubkey,
&decomp_data,
json_format,
)
.await
} else {
decompress_with_uploader_huge(
config,
&fee_payer_keypair,
&target_pubkey,
&decomp_data,
json_format,
)
.await
}
}
async fn decompress_direct(
config: &Config,
fee_payer_keypair: &thru_base::tn_tools::KeyPair,
target_pubkey: &thru_base::tn_tools::Pubkey,
decomp_data: &[u8],
state_proof_bytes: &[u8],
nonce: u64,
start_slot: u64,
chain_id: u16,
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info("Using direct decompression (data fits in single transaction)");
}
let transaction = TransactionBuilder::build_decompress_account(
fee_payer_keypair.public_key, SYSTEM_PROGRAM, target_pubkey.to_bytes()?, decomp_data, state_proof_bytes, 1, nonce, start_slot, )
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to build decompression transaction: {}", e))
})?;
if !json_format {
output::print_info("Signing transaction...");
}
let mut transaction = transaction.with_chain_id(chain_id);
transaction
.sign(&fee_payer_keypair.private_key)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to sign transaction: {}", e))
})?;
if !json_format {
output::print_info("Executing transaction...");
}
let client = create_rpc_client(config)?;
let transaction_bytes = transaction.to_wire();
let timeout = Duration::from_secs(config.timeout_seconds);
let transaction_details = client
.execute_transaction(&transaction_bytes, timeout)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to execute transaction: {}", e))
})?;
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, vm_error_label) = vm_error_strings(transaction_details.vm_error);
return Err(CliError::TransactionSubmission(format!(
"Transaction failed with execution result: {} (VM error: {}{}, User error: {})",
transaction_details.execution_result as i64,
vm_error_label,
vm_error_msg,
transaction_details.user_error_code
)));
}
if json_format {
let response = serde_json::json!({
"account_decompress": {
"fee_payer": fee_payer_keypair.address_string.as_str(),
"target_account": target_pubkey.as_str(),
"signature": transaction_details.signature.as_str(),
"status": "success",
"method": "direct",
"execution_result": transaction_details.execution_result,
"vm_error": transaction_details.vm_error,
"vm_error_name": vm_error_strings(transaction_details.vm_error).1,
"user_error_code": transaction_details.user_error_code,
"compute_units_consumed": transaction_details.compute_units_consumed,
"slot": transaction_details.slot
}
});
output::print_output(response, true);
} else {
output::print_success("Account decompression completed successfully");
println!("Fee payer: {}", fee_payer_keypair.address_string.as_str());
println!("Target account: {}", target_pubkey.as_str());
println!("Signature: {}", transaction_details.signature.as_str());
println!("Slot: {}", transaction_details.slot);
println!(
"Compute units consumed: {}",
transaction_details.compute_units_consumed
);
}
Ok(())
}
async fn decompress_with_uploader(
config: &Config,
fee_payer_keypair: &thru_base::tn_tools::KeyPair,
target_pubkey: &thru_base::tn_tools::Pubkey,
decomp_data: &[u8],
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info(
"Using uploader-based decompression (data too large for single transaction)",
);
}
let parsed_account_meta = TnAccountMeta::from_wire(&decomp_data[0..TN_ACCOUNT_META_FOOTPRINT])
.ok_or_else(|| CliError::Validation("Failed to parse account meta".to_string()))?;
if !json_format {
output::print_info(&format!("parsed account meta: {:?}", parsed_account_meta));
}
let uploader_program_pubkey = config.get_uploader_pubkey()?;
let mut seed_bytes = [3u8; 16];
rand::Rng::fill(&mut rand::rng(), &mut seed_bytes);
let seed = hex::encode(seed_bytes);
let (meta_account, buffer_account) =
crate::crypto::derive_uploader_accounts_from_seed(&seed, &uploader_program_pubkey)?;
if !json_format {
output::print_info(&format!("Creating ephemeral accounts for decompression:"));
output::print_info(&format!(" Meta account: {}", meta_account));
output::print_info(&format!(" Buffer account: {}", buffer_account));
}
let mut uploader_config = config.clone();
uploader_config.uploader_program_public_key = uploader_program_pubkey.to_string();
let uploader = crate::commands::uploader::UploaderManager::new(&uploader_config).await?;
const CHUNK_SIZE: usize = 31 * 1024;
let _upload_session = uploader
.upload_program(&seed, decomp_data, CHUNK_SIZE, json_format)
.await?;
if !json_format {
output::print_success("Decompression data uploaded successfully");
}
let client = create_rpc_client(config)?;
let meta_size = thru_base::tn_account::TN_ACCOUNT_META_FOOTPRINT;
let state_proof_config = MakeStateProofConfig {
proof_type: ProofType::Existing,
slot: None,
};
let state_proof_bytes = client
.make_state_proof(&target_pubkey, &state_proof_config)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to create state proof: {}", e))
})?;
if !json_format {
output::print_info(&format!("State proof size: {}", state_proof_bytes.len()));
}
let block_height = client.get_block_height().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get current block height: {}", e))
})?;
let start_slot = block_height.executed_height + 1;
let chain_info = client.get_chain_info().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get chain info: {}", e))
})?;
let fee_payer_account_info = client
.get_account_info(&fee_payer_keypair.address_string, None, None)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get fee payer account info: {}", e))
})?
.ok_or_else(|| {
CliError::Validation(format!(
"Fee payer account {} not found. Please ensure the account is funded.",
fee_payer_keypair.address_string.as_str()
))
})?;
let nonce = fee_payer_account_info.nonce;
if !json_format {
output::print_info(&format!("Decompression data size: {}", decomp_data.len()));
}
let mut transaction = TransactionBuilder::build_decompress2(
fee_payer_keypair.public_key,
SYSTEM_PROGRAM, target_pubkey.to_bytes()?,
buffer_account.to_bytes()?, buffer_account.to_bytes()?, meta_size as u32, &state_proof_bytes,
1,
nonce,
start_slot,
decomp_data.len() as u32,
)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to build DECOMPRESS2 transaction: {}", e))
})?;
transaction = transaction.with_chain_id(chain_info.chain_id);
transaction
.sign(&fee_payer_keypair.private_key)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to sign transaction: {}", e))
})?;
let transaction_bytes = transaction.to_wire();
let timeout = Duration::from_secs(config.timeout_seconds);
let transaction_details = client
.execute_transaction(&transaction_bytes, timeout)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to execute transaction: {}", e))
})?;
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, vm_error_label) = vm_error_strings(transaction_details.vm_error);
return Err(CliError::TransactionSubmission(format!(
"Transaction failed with execution result: {} (VM error: {}{}, User error: {})",
transaction_details.execution_result as i64,
vm_error_label,
vm_error_msg,
transaction_details.user_error_code
)));
}
if json_format {
let method = if decomp_data.len() > meta_size {
"uploader_separate_accounts"
} else {
"uploader_single_account"
};
let mut response = serde_json::json!({
"account_decompress": {
"fee_payer": fee_payer_keypair.address_string.as_str(),
"target_account": target_pubkey.as_str(),
"signature": transaction_details.signature.as_str(),
"status": "success",
"method": method,
"execution_result": transaction_details.execution_result,
"vm_error": transaction_details.vm_error,
"vm_error_name": vm_error_strings(transaction_details.vm_error).1,
"user_error_code": transaction_details.user_error_code,
"compute_units_consumed": transaction_details.compute_units_consumed,
"slot": transaction_details.slot
}
});
if decomp_data.len() > meta_size {
response["account_decompress"]["meta_account"] =
serde_json::Value::String(meta_account.to_string());
response["account_decompress"]["buffer_account"] =
serde_json::Value::String(buffer_account.to_string());
} else {
response["account_decompress"]["ephemeral_account"] =
serde_json::Value::String(buffer_account.to_string());
}
output::print_output(response, true);
} else {
if decomp_data.len() > meta_size {
output::print_success(
"Account decompression completed successfully using separate accounts",
);
println!("Meta account: {}", meta_account);
println!("Buffer account: {}", buffer_account);
} else {
output::print_success(
"Account decompression completed successfully using single ephemeral account",
);
println!("Ephemeral account: {}", buffer_account);
}
println!("Fee payer: {}", fee_payer_keypair.address_string.as_str());
println!("Target account: {}", target_pubkey.as_str());
println!("Signature: {}", transaction_details.signature.as_str());
println!("Slot: {}", transaction_details.slot);
println!(
"Compute units consumed: {}",
transaction_details.compute_units_consumed
);
}
if !json_format {
output::print_info("Cleaning up ephemeral accounts...");
}
match uploader.cleanup_program(&seed, json_format).await {
Ok(()) => {
if !json_format {
output::print_success("Ephemeral accounts cleaned up successfully");
}
}
Err(e) => {
if !json_format {
output::print_warning(&format!("Failed to clean up ephemeral accounts: {}", e));
}
}
}
Ok(())
}
async fn decompress_with_uploader_huge(
config: &Config,
fee_payer_keypair: &thru_base::tn_tools::KeyPair,
target_pubkey: &thru_base::tn_tools::Pubkey,
decomp_data: &[u8],
json_format: bool,
) -> Result<(), CliError> {
if !json_format {
output::print_info("Using uploader-based decompression for huge data (>16MB)");
}
let uploader_program_pubkey = config.get_uploader_pubkey()?;
let mut meta_seed_bytes = [1u8; 16];
rand::Rng::fill(&mut rand::rng(), &mut meta_seed_bytes);
let meta_seed = hex::encode(meta_seed_bytes);
let mut buffer_seed_bytes = [2u8; 16];
rand::Rng::fill(&mut rand::rng(), &mut buffer_seed_bytes);
let buffer_seed = hex::encode(buffer_seed_bytes);
let (_, meta_account) =
crate::crypto::derive_uploader_accounts_from_seed(&meta_seed, &uploader_program_pubkey)?;
let (_, buffer_account) =
crate::crypto::derive_uploader_accounts_from_seed(&buffer_seed, &uploader_program_pubkey)?;
if !json_format {
output::print_info(&format!(
"Creating ephemeral accounts for huge decompression:"
));
output::print_info(&format!(
" Meta account: {} (seed: {})",
meta_account, meta_seed
));
output::print_info(&format!(
" Buffer account: {} (seed: {})",
buffer_account, buffer_seed
));
}
let mut uploader_config = config.clone();
uploader_config.uploader_program_public_key = uploader_program_pubkey.to_string();
let uploader = crate::commands::uploader::UploaderManager::new(&uploader_config).await?;
if decomp_data.len() < TN_ACCOUNT_META_FOOTPRINT {
return Err(CliError::Validation(
"Decompression data is too small to contain metadata".to_string(),
));
}
let meta_data = &decomp_data[..TN_ACCOUNT_META_FOOTPRINT];
let buffer_data = &decomp_data[TN_ACCOUNT_META_FOOTPRINT..];
if !json_format {
output::print_info(&format!(
"Uploading metadata ({} bytes) to meta account",
meta_data.len()
));
}
const CHUNK_SIZE: usize = 31 * 1024;
let _meta_upload_session = uploader
.upload_program(&meta_seed, meta_data, CHUNK_SIZE, json_format)
.await?;
if !json_format {
output::print_info(&format!(
"Uploading buffer data ({} bytes) to buffer account",
buffer_data.len()
));
}
let buffer_uploader = crate::commands::uploader::UploaderManager::new(&uploader_config).await?;
let _buffer_upload_session = buffer_uploader
.upload_program(&buffer_seed, buffer_data, CHUNK_SIZE, json_format)
.await?;
if !json_format {
output::print_success("Huge decompression data uploaded successfully");
}
let client = create_rpc_client(config)?;
if !json_format {
output::print_info("Creating DECOMPRESS2 transaction for huge data");
}
let block_height = client.get_block_height().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get current block height: {}", e))
})?;
let start_slot = block_height.executed_height + 1;
let chain_info = client.get_chain_info().await.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get chain info: {}", e))
})?;
let fee_payer_account_info = client
.get_account_info(&fee_payer_keypair.address_string, None, None)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get fee payer account info: {}", e))
})?
.ok_or_else(|| {
CliError::Validation(format!(
"Fee payer account {} not found. Please ensure the account is funded.",
fee_payer_keypair.address_string.as_str()
))
})?;
let nonce = fee_payer_account_info.nonce;
let state_proof_config = MakeStateProofConfig {
proof_type: ProofType::Existing,
slot: None,
};
let state_proof_bytes = client
.make_state_proof(&target_pubkey, &state_proof_config)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to create state proof: {}", e))
})?;
let mut transaction = TransactionBuilder::build_decompress2(
fee_payer_keypair.public_key,
SYSTEM_PROGRAM, target_pubkey.to_bytes()?,
meta_account.to_bytes()?, buffer_account.to_bytes()?, 0, &state_proof_bytes,
1,
nonce,
start_slot,
buffer_data.len() as u32, )
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to build DECOMPRESS2 transaction: {}", e))
})?;
transaction = transaction.with_chain_id(chain_info.chain_id);
transaction
.sign(&fee_payer_keypair.private_key)
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to sign transaction: {}", e))
})?;
let transaction_bytes = transaction.to_wire();
let timeout = Duration::from_secs(60); let transaction_details = client
.execute_transaction(&transaction_bytes, timeout)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to execute transaction: {}", e))
})?;
if transaction_details.execution_result != 0 || transaction_details.vm_error != 0 {
let (vm_error_msg, vm_error_label) = vm_error_strings(transaction_details.vm_error);
return Err(CliError::TransactionSubmission(format!(
"Transaction failed with execution result: {} (VM error: {}{}, User error: {})",
transaction_details.execution_result as i64,
vm_error_label,
vm_error_msg,
transaction_details.user_error_code
)));
}
if json_format {
let response = serde_json::json!({
"account_decompress": {
"fee_payer": fee_payer_keypair.address_string.as_str(),
"target_account": target_pubkey.as_str(),
"signature": transaction_details.signature.as_str(),
"status": "success",
"method": "uploader_huge_separate_accounts",
"execution_result": transaction_details.execution_result,
"vm_error": transaction_details.vm_error,
"vm_error_name": vm_error_strings(transaction_details.vm_error).1,
"user_error_code": transaction_details.user_error_code,
"compute_units_consumed": transaction_details.compute_units_consumed,
"slot": transaction_details.slot,
"meta_account": meta_account.to_string(),
"buffer_account": buffer_account.to_string(),
"total_data_size": decomp_data.len(),
"meta_data_size": meta_data.len(),
"buffer_data_size": buffer_data.len()
}
});
output::print_output(response, true);
} else {
output::print_success(
"Huge account decompression completed successfully using separate accounts",
);
println!("Meta account: {}", meta_account);
println!("Buffer account: {}", buffer_account);
println!("Fee payer: {}", fee_payer_keypair.address_string.as_str());
println!("Target account: {}", target_pubkey.as_str());
println!("Signature: {}", transaction_details.signature.as_str());
println!("Slot: {}", transaction_details.slot);
println!(
"Compute units consumed: {}",
transaction_details.compute_units_consumed
);
println!("Total data size: {} bytes", decomp_data.len());
println!("Meta data size: {} bytes", meta_data.len());
println!("Buffer data size: {} bytes", buffer_data.len());
}
if !json_format {
output::print_info("Cleaning up ephemeral accounts...");
}
match uploader.cleanup_program(&meta_seed, json_format).await {
Ok(()) => {
if !json_format {
output::print_success("Meta account cleaned up successfully");
}
}
Err(e) => {
if !json_format {
output::print_warning(&format!("Failed to clean up meta account: {}", e));
}
}
}
match buffer_uploader
.cleanup_program(&buffer_seed, json_format)
.await
{
Ok(()) => {
if !json_format {
output::print_success("Buffer account cleaned up successfully");
}
}
Err(e) => {
if !json_format {
output::print_warning(&format!("Failed to clean up buffer account: {}", e));
}
}
}
Ok(())
}
async fn prepare_account_decompression(
config: &Config,
account: &str,
json_format: bool,
) -> Result<(), CliError> {
let client = create_rpc_client(config)?;
let account_pubkey = if account.starts_with("ta") && account.len() == 46 {
thru_base::tn_tools::Pubkey::new(account.to_string())
.map_err(|e| CliError::Validation(format!("Invalid account address: {}", e)))?
} else {
let private_key_hex = config.keys.get_key(account)
.map_err(|_| CliError::Validation(format!("Account '{}' not found in configuration. Use a ta... address or add the key to configuration.", account)))?;
let keypair = crate::crypto::keypair_from_hex(private_key_hex)?;
keypair.address_string
};
if !json_format {
output::print_info(&format!(
"Preparing decompression for account: {}",
account_pubkey.as_str()
));
}
match client.prepare_account_decompression(&account_pubkey).await {
Ok(response) => {
if json_format {
let result = serde_json::json!({
"status": "success",
"account": account_pubkey.as_str(),
"account_data": response.account_data,
"state_proof": response.state_proof
});
output::print_output(result, true);
} else {
output::print_success(&format!(
"Account decompression prepared for: {}",
account_pubkey.as_str()
));
println!("Account Data (base64): {}", response.account_data);
println!("State Proof (base64): {}", response.state_proof);
}
}
Err(e) => {
let error_msg = format!("Failed to prepare account decompression: {}", e);
if json_format {
let result = serde_json::json!({
"status": "error",
"account": account_pubkey.as_str(),
"error": error_msg
});
output::print_output(result, true);
} else {
output::print_error(&error_msg);
}
return Err(CliError::Validation(error_msg));
}
}
Ok(())
}
fn create_rpc_client(config: &Config) -> Result<Client, CliError> {
let rpc_url = config.get_grpc_url()?;
let timeout = Duration::from_secs(config.timeout_seconds);
ClientBuilder::new()
.http_endpoint(rpc_url)
.timeout(timeout)
.auth_token(config.auth_token.clone())
.build()
.map_err(|e| e.into())
}
fn vm_error_strings(code: i32) -> (String, String) {
let label = format_vm_error(code);
let message = if code != 0 {
format!(" (VM error: {})", label)
} else {
String::new()
};
(message, label)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_rpc_client() {
let config = Config::default();
let client = create_rpc_client(&config);
assert!(client.is_ok());
}
}