From ddecae8c4095142d3740643d27ad090b8f77a53e Mon Sep 17 00:00:00 2001 From: Sasha Date: Tue, 3 Mar 2026 23:21:18 +0100 Subject: [PATCH] feat: ensure pinata recipient initialized --- integration_tests/tests/pinata.rs | 112 ++++++++++++++++++++++++++++++ wallet/src/cli/programs/pinata.rs | 66 +++++++++++++++--- 2 files changed, 169 insertions(+), 9 deletions(-) diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 002dd2c7..da5f13c3 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -16,6 +16,118 @@ use wallet::cli::{ }, }; +#[test] +async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: winner_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + let winner_account_id_formatted = format_public_account_id(winner_account_id); + + let pinata_balance_pre = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to: winner_account_id_formatted, + }), + ) + .await; + + assert!( + claim_result.is_err(), + "Expected uninitialized account error" + ); + let err = claim_result.unwrap_err().to_string(); + assert!( + err.contains("wallet auth-transfer init --account-id Public/"), + "Expected init guidance, got: {err}", + ); + + let pinata_balance_post = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + assert_eq!(pinata_balance_post, pinata_balance_pre); + + Ok(()) +} + +#[test] +async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: winner_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + let winner_account_id_formatted = format_private_account_id(winner_account_id); + + let pinata_balance_pre = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to: winner_account_id_formatted, + }), + ) + .await; + + assert!( + claim_result.is_err(), + "Expected uninitialized account error" + ); + let err = claim_result.unwrap_err().to_string(); + assert!( + err.contains("wallet auth-transfer init --account-id Private/"), + "Expected init guidance, got: {err}", + ); + + let pinata_balance_post = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + assert_eq!(pinata_balance_post, pinata_balance_pre); + + Ok(()) +} + #[test] async fn claim_pinata_to_existing_public_account() -> Result<()> { let mut ctx = TestContext::new().await?; diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index b117c4c1..d7e28528 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use clap::Subcommand; use common::{PINATA_BASE58, transaction::NSSATransaction}; +use nssa::{Account, AccountId}; use crate::{ AccDecodeData::Decode, @@ -102,17 +103,17 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { pinata_account_id, winner_account_id, } => { - let pinata_account_id = pinata_account_id.parse().unwrap(); + let pinata_account_id = pinata_account_id.parse()?; + let winner_account_id: AccountId = winner_account_id.parse()?; + + ensure_public_recipient_initialized(wallet_core, winner_account_id).await?; + let solution = find_solution(wallet_core, pinata_account_id) .await .context("failed to compute solution")?; let res = Pinata(wallet_core) - .claim( - pinata_account_id, - winner_account_id.parse().unwrap(), - solution, - ) + .claim(pinata_account_id, winner_account_id, solution) .await?; println!("Results of tx send are {res:#?}"); @@ -138,8 +139,11 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { pinata_account_id, winner_account_id, } => { - let pinata_account_id = pinata_account_id.parse().unwrap(); - let winner_account_id = winner_account_id.parse().unwrap(); + let pinata_account_id = pinata_account_id.parse()?; + let winner_account_id: AccountId = winner_account_id.parse()?; + + ensure_private_owned_recipient_initialized(wallet_core, winner_account_id)?; + let solution = find_solution(wallet_core, pinata_account_id) .await .context("failed to compute solution")?; @@ -188,7 +192,51 @@ impl WalletSubcommand for PinataProgramSubcommand { } } -async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) -> Result { +async fn ensure_public_recipient_initialized( + wallet_core: &WalletCore, + winner_account_id: AccountId, +) -> Result<()> { + let account = wallet_core + .get_account_public(winner_account_id) + .await + .with_context(|| format!("failed to fetch recipient account Public/{winner_account_id}"))?; + + if account == Account::default() { + anyhow::bail!( + "Recipient account Public/{winner_account_id} is uninitialized.\n\ + Initialize it first:\n \ + wallet auth-transfer init --account-id Public/{winner_account_id}" + ); + } + + Ok(()) +} + +fn ensure_private_owned_recipient_initialized( + wallet_core: &WalletCore, + winner_account_id: AccountId, +) -> Result<()> { + let Some(account) = wallet_core.get_account_private(winner_account_id) else { + anyhow::bail!( + "Recipient account Private/{winner_account_id} is not found in this wallet.\n\ + `wallet pinata claim --to Private/...` supports owned private accounts only." + ); + }; + + if account == Account::default() { + anyhow::bail!( + "Recipient account Private/{winner_account_id} is uninitialized.\n\ + Initialize it first:\n \ + wallet auth-transfer init --account-id Private/{winner_account_id}\n\ + Then sync private state:\n \ + wallet account sync-private" + ); + } + + Ok(()) +} + +async fn find_solution(wallet: &WalletCore, pinata_account_id: AccountId) -> Result { let account = wallet.get_account_public(pinata_account_id).await?; let data: [u8; 33] = account .data