一文详细梳理Bank合约业务逻辑

  • Louis
  • 发布于 1天前
  • 阅读 184

上篇文章,我们使用Anchor工程化环境,从初始化项目、编译、部署、测试各个环节演示了一个真实的solana链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的Bank合约的源码。基于对源码和业务的的理解,我们后续可以扩展这个合约,设置一些更加复杂的功能。

文章背景:

上篇文章,我们使用 Anchor 工程化环境,从初始化项目、编译、部署、测试各个环节演示了一个真实的 solana 链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的 Bank 合约的源码。

基于对源码和业务的的理解,我们后续可以扩展这个合约,设置一些更加复杂的功能。

Bank 合约源码:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;

declare_id!("ditw8dH7D93kotkJgokM6WLbJHNdrbK9fJfLR74NJ7h");

#[program]
pub mod solana_bank {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Initializing bank contract");
        let bank = &mut ctx.accounts.bank;
        bank.owner = ctx.accounts.owner.key();
        bank.total_balance = 0;
        Ok(())
    }

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        const MIN_DEPOSIT: u64 = 10_000_000; // 0.01 SOL
        msg!(
            "Processing deposit of {} lamports from user: {}",
            amount,
            ctx.accounts.user.key()
        );
        require!(amount >= MIN_DEPOSIT, BankError::DepositTooSmall);

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.user.key(),
            &ctx.accounts.bank.key(),
            amount,
        );

        anchor_lang::solana_program::program::invoke(
            &transfer_instruction,
            &[
                ctx.accounts.user.to_account_info(),
                ctx.accounts.bank.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        let user_account = &mut ctx.accounts.user_account;
        let old_balance = user_account.balance;
        user_account.balance = user_account.balance.checked_add(amount).unwrap();

        let bank = &mut ctx.accounts.bank;
        bank.total_balance = bank.total_balance.checked_add(amount).unwrap();
        msg!(
            "Deposit successful. User balance: {} -> {}, Bank total: {}",
            old_balance,
            user_account.balance,
            bank.total_balance
        );

        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let user_account = &mut ctx.accounts.user_account;
        msg!(
            "Processing withdrawal of {} lamports for user: {}",
            amount,
            ctx.accounts.user.key()
        );
        require!(user_account.balance >= amount, BankError::InsufficientFunds);

        let old_balance = user_account.balance;
        let old_bank_balance = ctx.accounts.bank.total_balance;

        **ctx
            .accounts
            .bank
            .to_account_info()
            .try_borrow_mut_lamports()? -= amount;
        **ctx
            .accounts
            .user
            .to_account_info()
            .try_borrow_mut_lamports()? += amount;

        user_account.balance = user_account.balance.checked_sub(amount).unwrap();

        let bank = &mut ctx.accounts.bank;
        bank.total_balance = bank.total_balance.checked_sub(amount).unwrap();
        msg!(
            "Withdrawal successful. User balance: {} -> {}, Bank total: {} -> {}",
            old_balance,
            user_account.balance,
            old_bank_balance,
            bank.total_balance
        );

        Ok(())
    }

    pub fn get_balance(ctx: Context<GetBalance>) -> Result<u64> {
        let balance = ctx.accounts.user_account.balance;
        msg!(
            "Queried balance for user {}: {} lamports",
            ctx.accounts.user.key(),
            balance
        );
        Ok(balance)
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init_if_needed,
        payer = owner,
        space = 8 + 32 + 8,
        seeds = [b"bank"],
        bump
    )]
    pub bank: Account<'info, Bank>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut, seeds = [b"bank"], bump)]
    pub bank: Account<'info, Bank>,
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + 8,
        seeds = [b"user", user.key().as_ref()],
        bump
    )]
    pub user_account: Account<'info, UserAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut, seeds = [b"bank"], bump)]
    pub bank: Account<'info, Bank>,
    #[account(mut, seeds = [b"user", user.key().as_ref()], bump)]
    pub user_account: Account<'info, UserAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct GetBalance<'info> {
    #[account(seeds = [b"user", user.key().as_ref()], bump)]
    pub user_account: Account<'info, UserAccount>,
    pub user: Signer<'info>,
}

#[account]
pub struct Bank {
    pub owner: Pubkey,
    pub total_balance: u64,
}

#[account]
pub struct UserAccount {
    pub balance: u64,
}

#[error_code]
pub enum BankError {
    #[msg("Deposit amount must be at least 0.01 SOL")]
    DepositTooSmall,
    #[msg("Insufficient funds for withdrawal")]
    InsufficientFunds,
}

程序的核心功能:

初始化银行( initialize

  • 创建一个银行账户(PDA,种子 b"bank"),设置 owner 并初始化总余额 total_balance = 0
  • owner 支付账户初始化费用(rent)。

存款( deposit

  • 用户存入 SOL(最少 0.01 SOL,即 10_000_000 lamports)。
  • 使用 Solana 系统指令 system_instruction::transfer 完成转账。
  • 更新用户的 UserAccount 余额和银行总余额。

取款( withdraw

  • 用户提取 SOL,需确保余额足够。
  • 直接修改账户的 lamports(无需 invoke,更高效)。
  • 更新用户余额和银行总余额。

查询余额( get_balance

  • 返回用户的 UserAccount.balance

程序的核心业务并不复杂,如果说对于新手有难度和门槛的应该是对账户模型的理解,下面我们按照指令和账户约束的层面,从技术层面和业务层面来分析下源码:

initialize 指令:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    msg!("Initializing bank contract");
    let bank = &mut ctx.accounts.bank;
    bank.owner = ctx.accounts.owner.key();
    bank.total_balance = 0;
    Ok(())
}

1. 函数签名分析

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

语法层面

  • pub fn initialize:定义一个公开的(pub)函数 initialize
  • ctx: Context<Initialize>:接收一个 Context 参数,泛型类型是 Initialize(表示该函数只能由 Initialize 结构体定义的账户调用)。
  • -> Result<()>:返回 Anchor 的 Result 类型,() 表示无具体返回值(仅返回成功/错误状态)。

业务层面

  • 该函数用于 初始化银行合约,通常由合约的部署者(owner)调用。
  • Context<Initialize> 确保调用时必须传入符合 Initialize 结构体定义的账户(如 bankowner 等)。

2. 日志输出

msg!("Initializing bank contract");

语法层面

  • msg! 是 Anchor 提供的宏,用于在 Solana 链上打印日志(类似 println!)。
  • 日志内容会记录在交易日志中,便于调试和监控。

业务层面

  • 用于调试,标记合约初始化开始执行。

3. 获取 Bank 账户的可变引用

let bank = &mut ctx.accounts.bank;

语法层面

  • ctx.accounts.bank:从 Context 中获取 bank 账户(定义在 Initialize 结构体)。
  • &mut:获取可变引用(因为要修改 bank 的数据)。

业务层面

  • 这里操作的是 银行的主账户Bank 结构体实例),后续会设置 ownertotal_balance

4. 设置 Bank 的 owner

bank.owner = ctx.accounts.owner.key();

语法层面

  • bank.ownerBank 结构体的 owner 字段(类型是 Pubkey)。
  • ctx.accounts.owner.key():获取 owner 账户的公钥(Signer 类型的账户)。

业务层面

  • Bankowner 设置为调用者(ctx.accounts.owner),表示该银行合约的管理者。
  • 关键点

<!---->

    • owner 在后续可用于权限控制(如仅允许 owner 调用某些管理函数)。

5. 初始化 Bank 的总余额

bank.total_balance = 0;

语法层面

  • bank.total_balanceBank 结构体的 total_balance 字段(类型是 u64)。
  • = 0:初始化为 0(表示银行初始资金为 0)。

业务层面

  • 银行刚创建时,总存款(total_balance)应为 0
  • 后续 deposit/withdraw 会更新这个值。

6. 返回成功

Ok(())

语法层面

  • Ok(()):返回 Result::Ok,表示函数执行成功,无返回值。
  • 如果出错,可以返回 Err(BankError::SomeError)

业务层面

  • 表示初始化成功,合约可以正常使用。

Initialize 账户约束:

#[derive(Accounts)]
pub struct Initialize&lt;'info> {
    #[account(
        init_if_needed,
        payer = owner,
        space = 8 + 32 + 8,
        seeds = [b"bank"],
        bump
    )]
    pub bank: Account&lt;'info, Bank>,
    #[account(mut)]
    pub owner: Signer&lt;'info>,
    pub system_program: Program&lt;'info, System>,
}

结构体定义

#[derive(Accounts)]
pub struct Initialize&lt;'info> {

语法层面:

  • #[derive(Accounts)]:宏标记,表示该结构体是 Anchor 的 账户验证容器,用于定义指令的账户约束。
  • &lt;'info>:生命周期泛型,表示这些账户引用在交易执行期间有效。

语法层面:

  • 定义了 initialize 指令所需的账户集合,Anchor 会自动验证传入的账户是否符合这些约束。

银行主账户 bank

#[account(
    init_if_needed,
    payer = owner,
    space = 8 + 32 + 8,
    seeds = [b"bank"],
    bump
)]
pub bank: Account&lt;'info, Bank>,

语法层面:

  • #[account(...)]:属性宏,定义账户的初始化规则和安全约束。
  • init_if_needed:如果账户未初始化,则自动初始化;否则跳过(防重复初始化)。
  • payer = owner:初始化费用(rent)由 owner 账户支付。
  • space = 8 + 32 + 8:分配存储空间:

<!---->

    • 8:Anchor 的账户标识头。
    • 32Bank.ownerPubkey 类型,固定 32 字节)。
    • 8Bank.total_balanceu64 类型,固定 8 字节)。

<!---->

  • seeds = [b"bank"]:定义 PDA(Program Derived Address)的种子,此处为静态字符串 "bank"
  • bump:自动计算 PDA 的 bump 值(避免地址冲突)。

业务层面:

  • 创建或复用银行的全局状态账户,存储 ownertotal_balance
  • PDA 确保账户地址唯一性(通过 program_id + seeds 派生)。

调用者账户 owner

#[account(mut)]
pub owner: Signer&lt;'info>,

语法层面:

  • #[account(mut)]:标记该账户为 可变(因为要支付 rent,需修改 lamports)。
  • Signer&lt;'info>:要求 owner 必须对当前交易签名。

业务层面:

  • 调用者必须是真人钱包(具备签名能力)。
  • 支付银行账户的初始化费用(rent)。

系统程序 system_program

pub system_program: Program&lt;'info, System>,

语法层面:

  • Program&lt;'info, System>:显式声明依赖 Solana 系统程序。
  • 无需 mut,因为只读访问。

业务层面:

  • 用于执行账户初始化(init_if_needed 内部会调用系统程序)。
  • Anchor 要求所有涉及账户创建的操作必须传入 system_program

关键安全机制总结

属性/字段 语法作用 业务意义
init_if_needed 按需初始化账户 防止重复初始化,节省 rent 费用
payer = owner 指定支付者 调用者承担初始化成本 ...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis