Solana 60 天课程

2025年02月27日更新 80 人订阅
原价: ¥ 36 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 中的 Ed25519 签名验证 Solana 指令自省

基础银行教程

  • RareSkills
  • 发布于 2025-10-14 11:30
  • 阅读 372

本文介绍了如何在Solana上使用Anchor框架构建一个简单的银行程序,包括账户创建、余额查询、存款和取款等基本功能。文章详细讲解了程序中用到的关键概念,例如PDA(Program Derived Address),并通过Solidity代码和Rust代码进行了对比,展示了如何在Solana上实现类似以太坊的功能。

在本教程中,我们将在 Solana 上构建一个简单的银行程序,它具有你期望从普通银行获得的基本功能。用户可以创建账户、查看余额、存入资金,并在需要时提取资金。存入的 SOL 将存储在我们的程序拥有的银行 PDA 中。

以下是我们尝试用 Anchor 构建的 Solidity 表示。

在 Solidity 代码中,我们定义了一个 UserBank 结构体来保存状态。这些反映了我们将在 Solana 程序中创建的单独账户。代码中的 initialize 函数将银行的总存款设置为零,但在 Solana 程序中,这将涉及部署和初始化银行账户本身。类似地,Solidity 代码中的 createUserAccount 函数只是更新存储,而在 Solana 中,它将在链上创建并初始化一个新的用户账户。另请注意存款和取款如何更新用户和银行的状态,自定义错误会在余额不足或无效时强制执行规则。

// 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;
    }
}

创建基础银行程序

在深入研究代码之前,以下是我们的基础银行程序的工作方式。我们需要以下功能:

  • 一种设置和初始化银行的方法
  • 一种供用户创建自己的帐户的方法
  • 一种供用户存入资金的方法
  • 一种供用户提取资金的方法
  • 一种检查余额的方法

我们需要以下存储:

  • 一个中央账户,用于跟踪所有用户的总存款
  • 用户可以创建的个人账户,用于跟踪他们的余额和所有权

所有这些元素结合在一起构成了我们的基础银行程序。

现在,让我们开始吧。

银行 PDA 和用户账户

创建一个名为 basic_bank 的新 Anchor 项目,并添加以下程序代码。该程序定义了两个指令:initialize,它创建一个银行 PDA 来保存存款,以及 create_user_account,它设置一个特定于用户的 PDA 来跟踪每个用户的总存款。用户实际存入的 SOL 将存储在银行 PDA 中。

我们为用户提供了一个专用帐户,因为在 Solana 中,所有程序数据都必须作为其自身的帐户存在。与以太坊不同,我们可以使用映射来存储存款(mapping(address => deposited_amount)),Solana 需要显式帐户分配来存储状态。因此,我们创建一个 user_account PDA 来存储用户的地址和他们的存款金额。


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"); // 运行 ANCHOR SYNC 以更新你的程序 ID

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // 初始化银行账户
        let bank = &mut ctx.accounts.bank;
        bank.total_deposits = 0;

        msg!("银行已初始化");
        Ok(())
    }

    pub fn create_user_account(ctx: Context<CreateUserAccount>) -> Result<()> {
        // 初始化用户账户
        let user_account = &mut ctx.accounts.user_account;
        user_account.owner = ctx.accounts.user.key();
        user_account.balance = 0;

        msg!("已为以下用户创建账户: {:?}", user_account.owner);
        Ok(())
    }
}

让我们为我们的基础银行程序定义账户结构体。这些结构体被 initializecreate_user_account 函数使用,它们是:

  • Initialize****结构体,包含:

    • bank:正在创建的用于跟踪总存款的新银行账户
    • payer:为交易费用和租金支付的账户
    • system_program:创建新账户所必需的
  • CreateUserAccount 结构体包含:

    • bank:对主银行账户的引用
    • user_account:从用户的公钥派生的 PDA,用于存储他们的余额
    • user:拥有此账户并为其创建付费的签名者
    • system_program:创建账户所必需的
  • 最后,我们定义了一个 Bank 和一个 UserAccount 结构体,它们定义了各自账户的数据结构。Bank 结构体包含一个 total_deposits 字段,用于跟踪银行账户中的所有存款,而 UserAccount 结构体存储用户的公钥和他们在用户账户 PDA 中的个人余额。

// 账户结构体,用于创建银行 PDA 以进行存储
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(\
        init,\
        payer = payer,\
        space = 8 + Bank::INIT_SPACE)] // 鉴别器 + u64
    pub bank: Account<'info, Bank>,

    #[account(mut)]
    pub payer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

// 用于创建个人用户账户的账户结构体
#[derive(Accounts)]
pub struct CreateUserAccount<'info> {
    #[account(mut)]
    pub bank: Account<'info, Bank>,

    #[account(\
        init,\
        payer = user,\
        space = 8 + UserAccount::INIT_SPACE, // 鉴别器 + 公钥 + 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>,
}

// 用于跟踪所有用户的总存款的银行账户
#[account]
#[derive(InitSpace)]
pub struct Bank {
    pub total_deposits: u64,
}

// 用于跟踪个人用户余额的特定于用户的账户
#[account]
#[derive(InitSpace)]
pub struct UserAccount {
    pub owner: Pubkey,
    pub balance: u64,
}

接下来,添加以下测试以初始化并创建一个用户账户。

该测试执行以下操作:

  1. 为银行账户生成一个密钥对
  2. 使用提供者的钱包(我们的默认 Anchor 钱包)作为所有操作的签名者
  3. 设置测试金额值(存款 1 SOL,取款 0.5 SOL)
  4. 为签名者的公钥派生一个用户账户的 PDA
  5. 调用 initialize 以设置银行账户并验证它是否以零余额开始。
  6. 调用 createUserAccount 以初始化一个特定于用户的 PDA,并断言用户的地址和余额是否已正确记录。

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", () => {
  // 配置客户端以使用本地集群
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.BasicBank as Program<BasicBank>;
  const provider = anchor.AnchorProvider.env();

  // 为银行账户生成一个新的密钥对
  const bankAccount = Keypair.generate();

  // 使用提供者的钱包作为签名者
  const signer = provider.wallet;

  // 测试存款金额
  const depositAmount = new anchor.BN(1_000_000_000); // 1 SOL 以 lamports 为单位
  const withdrawAmount = new anchor.BN(500_000_000); // 0.5 SOL 以 lamports 为单位

  // 查找用户账户的 PDA
  const [userAccountPDA] = PublicKey.findProgramAddressSync(
    [Buffer.from("user-account"), signer.publicKey.toBuffer()],
    program.programId
  );

  it("初始化银行帐户", async () => {
    // 初始化银行帐户
    const tx = await program.methods
      .initialize()
      .accounts({
        bank: bankAccount.publicKey,
        payer: signer.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .signers([bankAccount])
      .rpc();

    console.log("初始化交易签名", tx);

    // 获取银行账户数据
    const bankData = await program.account.bank.fetch(bankAccount.publicKey);

    // 验证银行是否已正确初始化
    assert.equal(bankData.totalDeposits.toString(), "0");
  });

  it("创建一个用户账户", async () => {
    // 为签名者创建用户账户
    const tx = await program.methods
      .createUserAccount()
      .accounts({
        bank: bankAccount.publicKey,
        userAccount: userAccountPDA,
        user: signer.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    console.log("创建用户账户交易签名", tx);

    // 获取用户账户数据
    const userAccountData = await program.account.userAccount.fetch(userAccountPDA);

    // 验证用户账户是否已正确设置
    assert.equal(userAccountData.owner.toString(), signer.publicKey.toString());
    assert.equal(userAccountData.balance.toStri...

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

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

0 条评论

请先 登录 后评论