本文介绍了如何在Solana上使用Anchor框架构建一个简单的银行程序,包括账户创建、余额查询、存款和取款等基本功能。文章详细讲解了程序中用到的关键概念,例如PDA(Program Derived Address),并通过Solidity代码和Rust代码进行了对比,展示了如何在Solana上实现类似以太坊的功能。
在本教程中,我们将在 Solana 上构建一个简单的银行程序,它具有你期望从普通银行获得的基本功能。用户可以创建账户、查看余额、存入资金,并在需要时提取资金。存入的 SOL 将存储在我们的程序拥有的银行 PDA 中。
以下是我们尝试用 Anchor 构建的 Solidity 表示。
在 Solidity 代码中,我们定义了一个 User 和 Bank 结构体来保存状态。这些反映了我们将在 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;
}
}
在深入研究代码之前,以下是我们的基础银行程序的工作方式。我们需要以下功能:
我们需要以下存储:
所有这些元素结合在一起构成了我们的基础银行程序。
现在,让我们开始吧。
创建一个名为 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(())
}
}
让我们为我们的基础银行程序定义账户结构体。这些结构体被 initialize 和 create_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,
}
接下来,添加以下测试以初始化并创建一个用户账户。
该测试执行以下操作:
initialize 以设置银行账户并验证它是否以零余额开始。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... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!