En este tutorial, construiremos un programa de banco simple en Solana con las características básicas que esperarías de un banco regular. Los usuarios pueden crear cuentas, consultar saldos, depositar fondos y retirar sus fondos cuando lo necesiten. Los SOL depositados se almacenarán en un PDA del banco, que será propiedad de nuestro programa.
Aquí tienes una representación en Solidity de lo que estamos intentando construir con Anchor.
En el código de Solidity, definimos los structs User y Bank para mantener el estado. Estos reflejan las cuentas separadas que crearíamos en un programa de Solana. La función initialize en el código establece el depósito total del banco en cero, pero en un programa de Solana esto implicaría desplegar e inicializar la cuenta del banco en sí. De manera similar, la función createUserAccount en el código de Solidity simplemente actualiza el almacenamiento, mientras que en Solana crearía e inicializaría una nueva cuenta de usuario on-chain. También nota cómo los depósitos y retiros actualizan tanto el estado del usuario como el del banco, con errores personalizados que imponen las reglas cuando los saldos son insuficientes o inválidos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract BasicBank {
// Custom errors
error ZeroAmount();
error InsufficientBalance();
error Overflow();
error Underflow();
error InsufficientFunds();
error UnauthorizedAccess();
// Bank struct to track total deposits across all users
struct Bank {
uint256 totalDeposits;
}
// User account struct to store individual balances
struct UserAccount {
address owner;
uint256 balance;
}
// The bank state
Bank public bank;
// Mapping from user address to their user account
mapping(address => UserAccount) public userAccounts;
// Initialize the bank
function initialize() external {
bank.totalDeposits = 0;
}
// Create a user account
function createUserAccount() external {
// Ensure account doesn't already exist
require(userAccounts[msg.sender].owner == address(0), "Account already created");
// Initialize the user account
userAccounts[msg.sender].owner = msg.sender;
userAccounts[msg.sender].balance = 0;
}
// Deposit ETH into the bank
function deposit(uint256 amount) external payable {
// Ensure amount is greater than zero
if (amount == 0) {
revert ZeroAmount();
}
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
// Ensure the correct amount was sent
require(msg.value == amount, "Amount mismatch");
// Update user balance with checks for overflow
uint256 newUserBalance = userAccounts[msg.sender].balance + amount;
if (newUserBalance < userAccounts[msg.sender].balance) {
revert Overflow();
}
userAccounts[msg.sender].balance = newUserBalance;
// Update bank total deposits with checks for overflow
uint256 newTotalDeposits = bank.totalDeposits + amount;
if (newTotalDeposits < bank.totalDeposits) {
revert Overflow();
}
bank.totalDeposits = newTotalDeposits;
}
// Withdraw ETH from the bank
function withdraw(uint256 amount) external {
// Ensure amount is greater than zero
if (amount == 0) {
revert ZeroAmount();
}
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
// Check if user has enough balance
if (userAccounts[msg.sender].balance < amount) {
revert InsufficientBalance();
}
// Update user balance with checks for underflow
uint256 newBalance = userAccounts[msg.sender].balance - amount;
userAccounts[msg.sender].balance = newBalance;
// Update bank total deposits with checks for underflow
uint256 newTotalDeposits = bank.totalDeposits - amount;
if (newTotalDeposits > bank.totalDeposits) {
revert Underflow();
}
bank.totalDeposits = newTotalDeposits;
// Transfer ETH to the user
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Get the balance of the caller's bank account
function getBalance() external view returns (uint256) {
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
return userAccounts[msg.sender].balance;
}
}
Creando el programa Basic Bank
Antes de sumergirnos en el código, así es como funcionará nuestro programa de banco básico. Necesitaremos las siguientes funcionalidades:
- Una forma de configurar e inicializar el banco
- Una forma para que los usuarios creen sus propias cuentas
- Una forma para que los usuarios depositen fondos
- Una forma para que los usuarios retiren fondos
- Una forma de consultar saldos
Necesitaremos el siguiente almacenamiento:
- Una cuenta central para rastrear los depósitos totales de todos los usuarios
- Cuentas individuales, que pueden ser creadas por los usuarios para rastrear su saldo y propiedad
Todos estos elementos se unen para formar nuestro programa de banco básico.
Ahora, profundicemos.
PDA del banco y cuenta de usuario
Crea un nuevo proyecto de Anchor llamado basic_bank y agrega el código del programa a continuación. El programa define dos instrucciones: initialize, que crea un PDA del banco para mantener los depósitos, y create_user_account, que configura un PDA específico del usuario para rastrear los depósitos totales de cada usuario. Los SOL reales depositados por los usuarios se almacenarán en el PDA del banco.
Tenemos una cuenta dedicada para los usuarios porque en Solana, todos los datos del programa deben existir como su propia cuenta. A diferencia de Ethereum, donde podríamos usar un mapeo para almacenar depósitos (mapping(address => deposited_amount)), Solana requiere la asignación explícita de cuentas para almacenar el estado. Por lo tanto, creamos un PDA user_account para almacenar tanto la dirección del usuario como su cantidad de depósito.
use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent::Rent;
use anchor_lang::solana_program::system_instruction;
use anchor_lang::solana_program::program as solana_program;
declare_id!("u9tNA22L1oRZyF3RKoPVUYTAc1zCYSC5BySKFddZnfN"); // RUN ANCHOR SYNC TO UPDATE YOUR PROGRAM ID
#[program]
pub mod basic_bank {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Initialize the bank account
let bank = &mut ctx.accounts.bank;
bank.total_deposits = 0;
msg!("Bank initialized");
Ok(())
}
pub fn create_user_account(ctx: Context<CreateUserAccount>) -> Result<()> {
// Initialize the user account
let user_account = &mut ctx.accounts.user_account;
user_account.owner = ctx.accounts.user.key();
user_account.balance = 0;
msg!("User account created for: {:?}", user_account.owner);
Ok(())
}
}
Definamos los structs de cuentas para nuestro programa de banco básico. Estos structs son utilizados tanto por la función initialize como por create_user_account, y son:
- El struct
Initialize****, que contiene:bank: La nueva cuenta del banco que se está creando para rastrear los depósitos totalespayer: La cuenta que paga las tarifas de transacción y el alquiler (rent)system_program: Requerido para crear la nueva cuenta
- El struct
CreateUserAccountcontiene:bank: Referencia a la cuenta principal del bancouser_account: Un PDA derivado de la clave pública del usuario que almacena su saldouser: El firmante (signer) que es dueño de esta cuenta y paga por su creaciónsystem_program: Requerido para la creación de la cuenta
- Finalmente, definimos un struct
Banky unUserAccountque definen la estructura de datos de sus respectivas cuentas. El structBankcontiene un campototal_depositspara rastrear todos los depósitos en la cuenta del banco, mientras que el structUserAccountalmacena la clave pública de un usuario y su saldo individual en el PDA de la cuenta del usuario.
// ACCOUNT STRUCT TO CREATE THE BANK PDA TO STORE TO
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + Bank::INIT_SPACE)] // discriminator + u64
pub bank: Account<'info, Bank>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// ACCOUNT STRUCT FOR CREATING INDIVIDUAL USER ACCOUNT
#[derive(Accounts)]
pub struct CreateUserAccount<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
init,
payer = user,
space = 8 + UserAccount::INIT_SPACE, // discriminator + pubkey + u64
seeds = [b"user-account", user.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
// BANK ACCOUNT TO TRACK TOTAL DEPOSITS ACROSS ALL USERS
#[account]
#[derive(InitSpace)]
pub struct Bank {
pub total_deposits: u64,
}
// USER-SPECIFIC ACCOUNT TO TRACK INDIVIDUAL USER BALANCES
#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub owner: Pubkey,
pub balance: u64,
}
A continuación, agrega la siguiente prueba para inicializar y crear una cuenta de usuario.
La prueba hace lo siguiente:
- Genera un keypair para la cuenta del banco
- Usa la wallet del proveedor (nuestra wallet por defecto de Anchor) como el firmante (signer) para todas las operaciones
- Establece los valores de las cantidades de prueba (1 SOL para depósito, 0.5 SOL para retiro)
- Deriva un PDA para la cuenta de usuario usando la clave pública del firmante
- Llama a
initializepara configurar la cuenta del banco y verifica que comience con un saldo cero. - Llama a
createUserAccountpara inicializar un PDA específico del usuario y afirma (asserts) que la dirección y el saldo del usuario se registren correctamente.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Keypair, PublicKey } from "@solana/web3.js";
import { assert } from "chai";
import { BasicBank } from "../target/types/basic_bank";
describe("basic_bank", () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicBank as Program<BasicBank>;
const provider = anchor.AnchorProvider.env();
// Generate a new keypair for the bank account
const bankAccount = Keypair.generate();
// Use provider's wallet as the signer
const signer = provider.wallet;
// Test deposit amount
const depositAmount = new anchor.BN(1_000_000_000); // 1 SOL in lamports
const withdrawAmount = new anchor.BN(500_000_000); // 0.5 SOL in lamports
// Find PDA for user accounts
const [userAccountPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("user-account"), signer.publicKey.toBuffer()],
program.programId,
);
it("Initializes the bank account", async () => {
// Initialize the bank account
const tx = await program.methods
.initialize()
.accounts({
bank: bankAccount.publicKey,
payer: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([bankAccount])
.rpc();
console.log("Initialize transaction signature", tx);
// Fetch the bank account data
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
// Verify the bank is initialized correctly
assert.equal(bankData.totalDeposits.toString(), "0");
});
it("Creates a user account", async () => {
// Create user account for the signer
const tx = await program.methods
.createUserAccount()
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Create user account transaction signature", tx);
// Fetch the user account data
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
// Verify the user account is set correctly
assert.equal(userAccountData.owner.toString(), signer.publicKey.toString());
assert.equal(userAccountData.balance.toString(), "0");
});
});
Ejecuta la prueba, debería pasar exitosamente.

Ahora implementaremos el código para depositar fondos en el banco y escribiremos una prueba para ello.
Depositar SOL al banco
Agrega la función de depósito a continuación a nuestro programa.
Esta hace lo siguiente:
- Valida que la cantidad de depósito sea mayor que cero
- Crea y ejecuta una instrucción
transferdel System (vía CPI) para transferir SOL de la wallet del usuario a la cuenta del banco - Incrementa de forma segura el saldo de la cuenta del usuario sumando la cantidad depositada a su PDA
user_account - Actualiza los depósitos totales del banco con la misma cantidad
- Registra (logs) la cantidad depositada y la dirección del usuario
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// Ensure deposit amount is greater than zero
require!(amount > 0, BankError::ZeroAmount);
let user = &ctx.accounts.user.key();
let bank = &ctx.accounts.bank.key();
// Transfer SOL from user to bank account using System Program
let transfer_ix = system_instruction::transfer(user, bank, amount);
solana_program::invoke(
&transfer_ix,
&[
ctx.accounts.user.to_account_info(),
ctx.accounts.bank.to_account_info(),
],
)?;
// Update user balance
let user_account = &mut ctx.accounts.user_account;
user_account.balance = user_account
.balance
.checked_add(amount)
.ok_or(BankError::Overflow)?;
// Update bank total deposits
let bank = &mut ctx.accounts.bank;
bank.total_deposits = bank
.total_deposits
.checked_add(amount)
.ok_or(BankError::Overflow)?;
msg!("Deposited {} lamports for {}", amount, user);
Ok(())
}
Toma nota de cómo usamos la instrucción transfer del sistema para transferir SOL de la wallet del usuario hacia el banco; revisaremos este patrón más adelante.
Ahora, agrega el struct de cuentas Deposit
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
mut,
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess // Ensure the signer owns the account
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Agrega el BankError personalizado
#[error_code]
pub enum BankError {
#[msg("Amount must be greater than zero")]
ZeroAmount,
#[msg("Insufficient balance for withdrawal")]
InsufficientBalance,
#[msg("Arithmetic overflow")]
Overflow,
#[msg("Arithmetic underflow")]
Underflow,
#[msg("Insufficient funds in the bank account")]
InsufficientFunds,
#[msg("Unauthorized access to user account")]
UnauthorizedAccess,
}
Ahora actualiza la prueba del programa con el bloque de prueba a continuación.
La prueba de depósito hace lo siguiente:
- Registra los saldos iniciales de SOL tanto de la cuenta del usuario como la del banco
- Envía una transacción de depósito de 1 SOL del usuario al banco
- Verifica que tanto el registro del saldo de la cuenta del usuario como el registro del depósito total del banco se actualicen correctamente con la nueva cantidad de depósito
- Comprueba que el saldo del banco aumentó y el saldo del usuario disminuyó de forma adecuada (teniendo en cuenta las tarifas de transacción) para confirmar que las transferencias reales de SOL ocurrieron
- Finalmente, registra todos los cambios de saldo
it("Deposits funds into the bank", async () => {
// Get initial SOL balances
const initialUserBalance = await provider.connection.getBalance(signer.publicKey);
const initialBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Initial user SOL balance: ${initialUserBalance / 1e9} SOL`);
console.log(`Initial bank SOL balance: ${initialBankBalance / 1e9} SOL`);
// Deposit funds into the bank
const tx = await program.methods
.deposit(depositAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Deposit transaction signature", tx);
// Get the user's account balance
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
// Verify the tracked balance is correct
assert.equal(userAccountData.balance.toString(), depositAmount.toString());
// Verify bank total tracked deposits
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
assert.equal(bankData.totalDeposits.toString(), depositAmount.toString());
// Get final SOL balances
const finalUserBalance = await provider.connection.getBalance(signer.publicKey);
const finalBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Final user SOL balance: ${finalUserBalance / 1e9} SOL`);
console.log(`Final bank SOL balance: ${finalBankBalance / 1e9} SOL`);
// Check actual SOL transfers (accounting for tx fees)
assert.isTrue(finalBankBalance > initialBankBalance);
// User balance should be reduced by deposit amount + some tx fees
assert.isTrue(finalUserBalance < initialUserBalance - Number(depositAmount));
assert.isTrue(finalUserBalance > initialUserBalance - Number(depositAmount) - 10000); // Account for a reasonable tx fees
});
Ejecuta la prueba, y pasa exitosamente.

A continuación, agregaremos una función para recuperar la cantidad depositada por un usuario en nuestro banco.
Obtener el saldo del usuario
La función get_balance a continuación recupera y devuelve el saldo en lamports para un usuario.
pub fn get_balance(ctx: Context<GetBalance>) -> Result<u64> {
// Get user account
let user_account = &ctx.accounts.user_account;
let balance = user_account.balance;
msg!("Balance for {}: {} lamports", user_account.owner, balance);
Ok(balance)
}
Agrega el struct de cuentas GetBalance.
#[derive(Accounts)]
pub struct GetBalance<'info> {
pub bank: Account<'info, Bank>,
#[account(
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess // Ensure the signer owns the account
)]
pub user_account: Account<'info, UserAccount>,
pub user: Signer<'info>,
}
Agrega la prueba para la función.
La prueba invoca la función getBalance de nuestro programa en Anchor y afirma que el saldo devuelto es igual a la cantidad que depositamos anteriormente (1 SOL).
it("Retrieves user balance", async () => {
// Get the user's balance
const balance = await program.methods
.getBalance()
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
})
.view(); // The .view() method in Anchor is used to call instructions that only read data (view functions) without submitting an actual transaction
// Verify the balance is correct
assert.equal(balance.toString(), depositAmount.toString());
console.log(`User balance: ${Number(balance) / 1e9} SOL`);
});
Ejecuta la prueba, pasa exitosamente.

Ahora, agregaremos la implementación de retiro y escribiremos su respectiva prueba.
Retirar saldo del banco: Caso de éxito
Agrega el código a continuación para retirar los depósitos de los usuarios de nuestro banco.
La función de retiro hace lo siguiente:
- Valida que la cantidad de retiro sea mayor que cero
- Comprueba si el usuario tiene saldo suficiente para el retiro
- Actualiza el saldo de la cuenta del usuario y los depósitos totales del banco usando aritmética verificada (checked arithmetic)
- Calcula el saldo mínimo necesario para mantener la cuenta del banco exenta de alquiler (rent-exempt)
- Determina la cantidad de transferencia segura desde el banco que preserve el mínimo exento de alquiler
- Transfiere SOL del banco a la wallet del usuario usando manipulación directa de lamports (ya que nuestro programa es dueño de la cuenta del banco)
- Registra los detalles del retiro
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Ensure withdraw amount is greater than zero
require!(amount > 0, BankError::ZeroAmount);
// Get accounts
let bank = &mut ctx.accounts.bank;
let user_account = &mut ctx.accounts.user_account;
let user = ctx.accounts.user.key();
// Check if the user has enough balance
require!(
user_account.balance >= amount,
BankError::InsufficientBalance
);
// Update user balance
user_account.balance = user_account
.balance
.checked_sub(amount)
.ok_or(BankError::Underflow)?;
// Update bank total deposits
bank.total_deposits = bank
.total_deposits
.checked_sub(amount)
.ok_or(BankError::Underflow)?;
// The bank account must keep enough lamports to stay rent-exempt,
// otherwise the runtime will garbage-collect it.
let rent = Rent::get()?;
let bank_account_info = ctx.accounts.bank.to_account_info();
let minimum_balance = rent.minimum_balance(bank_account_info.data_len());
// Only transfer what the bank can afford after reserving rent.
// Cap at the requested amount so we never send more than asked.
let available_lamports = bank_account_info.lamports();
let transfer_amount = amount.min(available_lamports.saturating_sub(minimum_balance));
// Transfer SOL: subtract from bank account and add to user wallet
**bank_account_info.try_borrow_mut_lamports()? -= transfer_amount;
**ctx.accounts.user.try_borrow_mut_lamports()? += transfer_amount;
msg!("Withdrawn {} lamports for {}", amount, user);
Ok(())
}
Recuerda que en la función deposit, usamos la función transfer del System Program para transferir SOL de la cuenta del usuario hacia el banco. Hacemos esto porque el System Program es dueño de todas las wallets regulares (como las EOA en Ethereum) y tiene permiso para modificar sus saldos.

Pero en la función withdraw, podemos modificar los saldos en lamports tanto del banco como del PDA de la cuenta de usuario directamente. Esto se debe a que nuestro programa es dueño de ambos PDAs (nosotros los desplegamos).

Ahora agrega el struct de cuentas Withdraw
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
mut,
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Agrega la prueba para la función de retiro.
Esta hace lo siguiente:
- Registra los saldos iniciales de la cuenta de usuario y obtiene los saldos de SOL tanto del usuario como del banco
- Envía una transacción de retiro de 0.5 SOL del banco al usuario
- Verifica que tanto el saldo de la cuenta de usuario como los depósitos totales del banco se actualicen correctamente después del retiro
- Comprueba que el saldo del usuario haya aumentado (teniendo en cuenta las tarifas de transacción) para confirmar que las transferencias reales de SOL ocurrieron
- Finalmente, registramos todos los cambios de saldo para la transacción
it("Withdraws funds from the bank", async () => {
// Get the initial balance
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
const initialBalance = userAccountData.balance;
// Get initial SOL balances
const initialUserBalance = await provider.connection.getBalance(signer.publicKey);
const initialBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Initial user SOL balance: ${initialUserBalance / 1e9} SOL`);
console.log(`Initial bank SOL balance: ${initialBankBalance / 1e9} SOL`);
// Withdraw funds from the bank
const tx = await program.methods
.withdraw(withdrawAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Withdraw transaction signature", tx);
// Get the new balance
const updatedUserAccountData = await program.account.userAccount.fetch(userAccountPDA);
const newBalance = updatedUserAccountData.balance;
// Verify the balance is correct
const expectedBalance = initialBalance.sub(withdrawAmount);
assert.equal(newBalance.toString(), expectedBalance.toString());
// Verify bank total deposits
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
assert.equal(bankData.totalDeposits.toString(), expectedBalance.toString());
// Get final SOL balances
const finalUserBalance = await provider.connection.getBalance(signer.publicKey);
const finalBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Final user SOL balance: ${finalUserBalance / 1e9} SOL`);
console.log(`Final bank SOL balance: ${finalBankBalance / 1e9} SOL`);
// Check actual SOL transfers
// User balance should increase by withdraw amount (minus tx fees)
// Since the user pays tx fees, the final balance might be slightly less than expected
assert.isTrue(finalUserBalance < initialUserBalance + Number(withdrawAmount));
assert.isTrue(finalUserBalance > initialUserBalance - 10000); // Allow for reasonable tx fees
// Bank balance should decrease by withdraw amount
assert.isTrue(finalBankBalance <= initialBankBalance);
});
Ahora ejecuta la prueba. Pasa exitosamente.

Retirar saldo del banco: Caso de falla
Agreguemos un bloque de prueba más para confirmar que los usuarios no pueden retirar más de lo que depositaron.
Agrega el siguiente código, el cual hace lo siguiente:
- Intenta retirar una cantidad excesiva (10 SOL) del banco
- Espera que la transacción falle con un error de saldo insuficiente
- Usa un bloque try/catch para capturar la falla esperada y registra el mensaje de error real recibido
- Verifica que el error contenga el texto “insufficient balance”
- Falla la prueba si no se lanza ningún error (lo cual indicaría una validación faltante — lo que no queremos)
it("Prevents users from withdrawing more than their balance", async () => {
// Try to withdraw more than the balance
const excessiveWithdrawAmount = new anchor.BN(10_000_000); // 10 SOL
try {
await program.methods
.withdraw(excessiveWithdrawAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// If we reach here, the test failed
assert.fail("Should have thrown an error for insufficient balance");
} catch (error) {
// Log the actual error
console.log("Error received:", error.toString());
// Check for multiple possible error messages that could indicate insufficient balance
const errorMsg = error.toString().toLowerCase();
assert.isTrue(
errorMsg.includes("insufficient balance") ||
errorMsg.includes("0x7d3")
);
}
});
Finalmente, ejecuta la prueba. Pasa exitosamente.

Esto concluye nuestro programa de banco básico.
Este artículo es parte de una serie de tutoriales sobre Solana.