本文详细介绍了 Solana 区块链上的 Escrow 程序的概念和实现,Escrow 程序通过智能合约持有资产直到满足预定义的条件,从而实现无需信任的交易。文章还提供了一个使用 Anchor 框架构建 Solana Escrow 程序的完整示例,包括初始化、存款、交换、取消和退款五个关键指令。
Solana 托管程序是一个基本概念,用于促进 Solana 区块链上各方之间无需信任的交易。与需要信任的传统交易不同,托管通过持有资产直到满足预定义条件,从而消除了对中间人的需求。
托管程序用于为区块链上的点对点交易带来安全性、透明性和自动化。
以下是使用它的原因:
托管使各方能够在不信任彼此或第三方的情况下进行交易。智能合约持有双方的资产,直到满足交易条件,确保任何一方都不能作弊。
仅当满足特定条件时才释放资产,例如:
托管支持自动化的交易逻辑,例如:
托管合约可防止:
我们学习它们是因为它们是 Solana 上安全、无需信任的应用程序的基础。
以下是它如此重要的原因:
托管是许多去中心化应用程序的核心:
托管教会你如何构建系统,在这些系统中,代码而非信任决定了各方之间的交互。
了解托管可以帮助你理解真实的项目如何促进安全交易,这对于任何交易应用程序来说都是关键部分。
学习托管对于理解安全交易模式、状态管理和攻击预防至关重要。
托管不是简单的存储帐户。它们是复杂的智能合约,可执行复杂的业务逻辑并确保公平交易。
如果在指定的时间范围内未满足条件,则托管可能具有不同的解决机制——并非所有托管都保证退款。
即使是基本的点对点交易也可以从托管模式中受益,尤其是在处理有价值的资产或不信任的各方时。
虽然托管降低了交易对手风险,但它们并不能消除所有风险,例如智能合约错误、预言机故障或经济攻击。
现在我们了解了什么是托管以及它们为何如此重要,让我们深入研究实际的实现。我们将构建一个全面的代币托管,通过五个关键指令演示所有核心概念:initialize(初始化)、deposit(存入)、exchange(交换)、cancel(取消)和 refund(退款)。
在我们开始编码之前,请确保你已具备:
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
)首先,让我们创建一个新的 Anchor 项目:
anchor init token_escrow
cd token_escrow
让我们从主程序文件开始。打开 programs/escrow/src/lib.rs
:
use anchor_lang::prelude::*;
declare_id!("ABagojQQU4h1roF1U2ZC2vqvMVrWcBx2gCq1Gy95KEvJ");
pub mod instructions;
pub mod states;
pub use instructions::*;
#[program]
pub mod escrow {
use super::*;
pub fn make(ctx: Context<Make>, seed: u64, amount: u64, receive: u64) -> Result<()> {
ctx.accounts.init_escrow(seed, &ctx.bumps, receive)?;
ctx.accounts.deposit(amount)
}
pub fn refund(ctx: Context<Refund>) -> Result<()> {
ctx.accounts.refund_and_close_vault()
}
pub fn take(ctx: Context<Take>) -> Result<()> {
ctx.accounts.deposit()?;
ctx.accounts.transfer_and_close_vault()
}
}
创建 programs/escrow/src/states/mod.rs
:
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Escrow {
pub seed: u64, // For indexing multiple escrows
pub maker: Pubkey, // Creator of the escrow
pub mint_a: Pubkey, // Token the maker is offering
pub mint_b: Pubkey, // Token the maker expects
pub receive: u64, // Amount of Token B expected
pub bump: u8, // PDA bump
}
#[derive(InitSpace)]
macro 会自动计算我们的帐户所需的空间,从而更容易管理帐户大小。
创建 programs/escrow/src/instructions/make.rs
:
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{ Mint, TokenAccount, TransferChecked, TokenInterface, transfer_checked}
};
use crate::states::Escrow;
#[derive(Accounts)]
#[instruction(seed: u64)]
pub struct Make<'info> {
/// The escrow creator who deposits tokens and pays for account creation.
/// Must be mutable to deduct lamports for rent and transaction fees.
/// 托管的创建者,他们存入代币并支付帐户创建费用。
/// 必须是可变的,以扣除租金和交易费用的 lamports。
#[account(mut)]
pub maker: Signer<'info>,
/// Token mint for the asset being deposited into escrow (Token A).
/// Validates compatibility with the specified token program.
/// 用于存入托管的资产的代币 铸币(代币 A)。
/// 验证与指定代币程序的兼容性。
#[account(\
mint::token_program = token_program\
)]
pub mint_a: InterfaceAccount<'info, Mint>,
/// Token mint for the asset expected in return (Token B).
/// Used for validation and stored in escrow state for future verification.
/// 预期返回的资产的代币 铸币(代币 B)。
/// 用于验证,并存储在托管状态中以供将来验证。
#[account(\
mint::token_program = token_program\
)]
pub mint_b: InterfaceAccount<'info, Mint>,
/// Maker's token account holding Token A to be escrowed.
/// Must have sufficient balance for the deposit amount.
/// 制造商的代币帐户,持有要托管的代币 A。
/// 必须有足够的余额来支付存款金额。
#[account(\
mut,\
associated_token::mint = mint_a,\
associated_token::authority = maker,\
associated_token::token_program = token_program\
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
/// Escrow state account storing trade parameters and metadata.
/// - Contains maker, token mints, expected amount, and bump seed
/// - Derived from maker's pubkey and user-provided seed for uniqueness
/// 托管状态帐户,存储交易参数和元数据。
/// - 包含 制造商, 代币 铸币, 预期金额, 和 bump seed
/// - 来源于 制造商 的pubkey 和 用户提供的seed 保证唯一性
#[account(\
init,\
payer = maker,\
seeds = [b"escrow", maker.key().as_ref(), seed.to_le_bytes().as_ref()],\
bump,\
space = 8 + Escrow::INIT_SPACE,\
)]
pub escrow: Account<'info, Escrow>,
/// Vault token account that holds escrowed Token A.
/// - Owned by the escrow PDA to prevent unauthorized access
/// - Created as ATA for deterministic address derivation
/// 用于保存托管的代币 A 的 Vault 代币帐户。
/// - 由托管的 PDA 拥有,以防止未经授权的访问
/// - 创建为 ATA 以实现确定性地址推导
#[account(\
init,\
payer = maker,\
associated_token::mint = mint_a,\
associated_token::authority = escrow,\
associated_token::token_program = token_program\
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
/// Token program interface for SPL token operations.
/// Supports both Token Program and Token-2022 for flexibility.
/// 用于 SPL 代币操作的代币程序接口。
/// 支持代币程序和 Token-2022 以实现灵活性。
pub token_program: Interface<'info, TokenInterface>,
/// Associated Token Program for creating and managing ATAs.
/// 用于创建和管理 ATA 的关联代币程序。
pub associated_token_program: Program<'info, AssociatedToken>,
/// System Program required for account creation and rent payments.
/// 帐户创建和租金支付所需的系统程序。
pub system_program: Program<'info, System>,
}
impl<'info> Make<'info> {
/// Initializes the escrow account with trade parameters.
///
/// Stores all necessary information for future trade execution including
/// maker identity, token mints, expected receive amount, and PDA bump.
/// 使用交易参数初始化托管帐户。
///
/// 存储将来交易执行所需的所有必要信息,包括
/// 制造商身份、代币 铸币、预期接收金额和 PDA bump。
pub fn init_escrow(&mut self, seed: u64, bump: &MakeBumps, receive: u64) -> Result<()>{
self.escrow.set_inner(Escrow {
seed,
maker: self.maker.key(),
mint_a: self.mint_a.key(),
mint_b: self.mint_b.key(),
receive,
bump: bump.escrow
});
Ok(())
}
/// Deposits Token A from maker's account into the escrow vault.
///
/// Uses transfer_checked for enhanced security with decimal validation.
/// Tokens remain locked until trade completion or refund.
/// 将代币 A 从 制造商的帐户存入托管 vault。
///
/// 使用 transfer_checked 进行增强的安全性,并进行十进制验证。
/// 代币将保持锁定状态,直到交易完成或退款。
pub fn deposit(&mut self, amount: u64) -> Result<()> {
let decimals = self.mint_a.decimals;
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked{
mint: self.mint_a.to_account_info(),
from: self.maker_ata_a.to_account_info(),
to: self.vault.to_account_info(),
authority: self.maker.to_account_info(),
};
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
// Transfer tokens with decimal validation for security
// 传输代币,并进行十进制验证以确保安全
transfer_checked(cpi_context, amount, decimals)
}
}
要点:
transfer_checked
进行增强的安全性,并进行十进制验证创建 programs/escrow/src/instructions/take.rs
:
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{
close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
TransferChecked,
},
};
use crate::states::Escrow;
/// Instruction for completing an escrow trade.
///
/// Process:
/// 1. Taker sends Token B to maker
/// 2. Escrowed Token A is transferred to taker
/// 3. Vault and escrow accounts are closed
/// 用于完成托管交易的指令。
///
/// 流程:
/// 1. 交易接受者将代币 B 发送给 制造商
/// 2. 托管的代币 A 被转移给交易接受者
/// 3. Vault 和托管帐户已关闭
#[derive(Accounts)]
pub struct Take<'info> {
/// The trade counterparty who provides Token B and receives Token A.
/// Must be mutable to receive tokens and pay for account creation.
/// 提供代币 B 并接收代币 A 的交易对手。
/// 必须是可变的,才能接收代币并支付帐户创建费用。
#[account(mut)]
pub taker: Signer<'info>,
/// Original escrow creator who receives Token B.
/// Must be mutable to receive rent refunds from closed accounts.
/// 接收代币 B 的原始托管创建者。
/// 必须是可变的,才能从已关闭的帐户接收租金退款。
#[account(mut)]
pub maker: SystemAccount<'info>,
/// Token mint for the asset taker provides (Token B).
/// Must match the mint stored in escrow state.
/// 交易接受者提供的资产的代币 铸币(代币 B)。
/// 必须与托管状态中存储的 铸币 相匹配。
#[account(\
mint::token_program = token_program\
)]
pub mint_b: InterfaceAccount<'info, Mint>,
/// Token mint for the asset taker receives (Token A).
/// Must match the mint stored in escrow state.
/// 交易接受者接收的资产的代币 铸币(代币 A)。
/// 必须与托管状态中存储的 铸币 相匹配。
#[account(\
mint::token_program = token_program\
)]
pub mint_a: InterfaceAccount<'info, Mint>,
/// Taker's token account holding Token B for payment.
/// Must have sufficient balance for the required amount.
/// 交易接受者的代币帐户,持有代币 B 用于付款。
/// 必须有足够的余额来支付所需的金额。
#[account(\
mut,\
associated_token::mint = mint_b,\
associated_token::authority = taker,\
associated_token::token_program = token_program\
)]
pub taker_ata_b: Box<InterfaceAccount<'info, TokenAccount>>,
/// Maker's token account to receive Token B.
/// Created automatically if it doesn't exist (taker pays rent).
/// 制造商的代币帐户,用于接收代币 B。
/// 如果它不存在,则会自动创建(交易接受者支付租金)。
#[account(\
init_if_needed,\
payer = taker,\
associated_token::mint = mint_b,\
associated_token::authority = maker,\
associated_token::token_program = token_program\
)]
pub maker_ata_b: Box<InterfaceAccount<'info, TokenAccount>>,
/// Taker's token account to receive Token A.
/// Created automatically if it doesn't exist (taker pays rent).
/// 交易接受者的代币帐户,用于接收代币 A。
/// 如果它不存在,则会自动创建(交易接受者支付租金)。
#[account(\
init_if_needed,\
payer = taker,\
associated_token::mint = mint_a,\
associated_token::authority = taker,\
associated_token::token_program = token_program\
)]
pub taker_ata_a: Box<InterfaceAccount<'info, TokenAccount>>,
/// Escrow state account containing trade parameters.
/// - Validates token mints and maker identity match stored values
/// - Rent is returned to maker upon closing
/// - Uses stored bump for PDA verification
/// 托管状态帐户,包含交易参数。
/// - 验证代币 铸币 和 制造商身份是否与存储的值匹配
/// - 关闭后,租金将退还给 制造商
/// - 使用存储的 bump 进行 PDA 验证
#[account(\
mut,\
close = maker,\
has_one = mint_a,\
has_one = maker,\
has_one = mint_b,\
seeds = [b"escrow", maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],\
bump = escrow.bump,\
)]
pub escrow: Account<'info, Escrow>,
/// Vault holding escrowed Token A.
/// Will be emptied and closed during trade execution.
/// Vault 持有托管的代币 A。
/// 将在交易执行期间清空并关闭。
#[account(\
mut,\
associated_token::mint = mint_a,\
associated_token::authority = escrow,\
associated_token::token_program = token_program\
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
impl<'info> Take<'info> {
/// Transfers Token B from taker to maker as payment.
///
/// Uses the expected receive amount stored in escrow state.
/// This completes the taker's side of the trade agreement.
/// 将代币 B 从交易接受者转移到 制造商作为付款。
///
/// 使用托管状态中存储的预期接收金额。
/// 这完成了交易接受者在交易协议中的部分。
pub fn deposit(&mut self) -> Result<()> {
let decimals = self.mint_b.decimals;
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked {
mint: self.mint_b.to_account_info(),
from: self.taker_ata_b.to_account_info(),
to: self.maker_ata_b.to_account_info(),
authority: self.taker.to_account_info(),
};
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
// Transfer the exact amount expected by the maker
// 转移 制造商期望的确切金额
transfer_checked(cpi_context, self.escrow.receive, decimals)?;
Ok(())
}
/// Transfers escrowed Token A to taker and closes the vault.
///
/// Completes the trade by releasing all escrowed tokens to the taker
/// and cleaning up the vault account (rent goes to maker).
/// 将托管的代币 A 转移给交易接受者并关闭 vault。
///
/// 通过将所有托管的代币释放给交易接受者来完成交易
/// 并清理 vault 帐户(租金转给 制造商)。
pub fn transfer_and_close_vault(&mut self) -> Result<()> {
// Create PDA signer seeds for vault authority
// 为 vault 授权机构创建 PDA 签名者种子
let signer_seeds: &[&[&[u8]]; 1] = &[&[\
b"escrow",\
self.maker.key.as_ref(),\
&self.escrow.seed.to_le_bytes()[..],\
&[self.escrow.bump],\
]];
let decimals = self.mint_a.decimals;
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: self.vault.to_account_info(),
mint: self.mint_a.to_account_info(),
to: self.taker_ata_a.to_account_info(),
authority: self.escrow.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
// Transfer all escrowed tokens to taker
// 将所有托管的代币转账给交易接受者
transfer_checked(cpi_context, self.vault.amount, decimals)?;
// Close vault account and refund rent to maker
// 关闭 vault 帐户并将租金退还给 制造商
let close_accounts = CloseAccount {
account: self.vault.to_account_info(),
destination: self.maker.to_account_info(),
authority: self.escrow.to_account_info(),
};
let close_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
close_accounts,
signer_seeds,
);
close_account(close_cpi_ctx)?;
Ok(())
}
}
要点:
init_if_needed
自动创建它们创建 programs/escrow/src/instructions/refund.rs
:
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{
close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
TransferChecked,
},
};
use crate::states::Escrow;
#[derive(Accounts)]
pub struct Refund<'info> {
/// The original escrow creator requesting refund of their tokens.
/// Must be mutable to receive refunded tokens and rent.
/// 请求退还其代币的原始托管创建者。
/// 必须是可变的,才能接收退还的代币和租金。
#[account(mut)]
pub maker: Signer<'info>,
/// Token mint for the escrowed asset (Token A).
/// Must match the mint stored in escrow state.
/// 托管资产的代币 铸币(代币 A)。
/// 必须与托管状态中存储的 铸币 相匹配。
#[account(\
mint::token_program = token_program\
)]
pub mint_a: InterfaceAccount<'info, Mint>,
/// Maker's token account to receive refunded Token A.
/// Must be the same account that originally funded the escrow.
/// 制造商的代币帐户,用于接收退还的代币 A。
/// 必须是最初为托管提供资金的同一帐户。
#[account(\
mut,\
associated_token::mint = mint_a,\
associated_token::authority = maker,\
associated_token::token_program = token_program\
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
/// Escrow state account to be closed after refund.
/// - Validates maker identity and token mint match stored values
/// - Rent is returned to maker upon closing
/// - Uses stored bump for PDA verification
/// 托管状态帐户将在退款后关闭。
/// - 验证 制造商 身份和代币 铸币 是否与存储的值匹配
/// - 关闭后,租金将退还给 制造商
/// - 使用存储的 bump 进行 PDA 验证
#[account(\
mut,\
close = maker,\
has_one = mint_a,\
has_one = maker,\
seeds = [b"escrow", maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],\
bump = escrow.bump,\
)]
pub escrow: Account<'info, Escrow>,
/// Vault holding escrowed Token A to be refunded.
/// Will be emptied and closed during refund process.
/// vault 持有要退还的托管的代币 A。
/// 将在退款过程中清空并关闭。
#[account(\
mut,\
associated_token::mint = mint_a,\
associated_token::authority = escrow,\
associated_token::token_program = token_program\
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
impl<'info> Refund<'info> {
/// Refunds all escrowed tokens to maker and closes the vault.
///
/// Returns the entire vault balance to the original maker,
/// effectively canceling the escrow and cleaning up accounts.
/// 将所有托管的代币退还给 制造商并关闭 vault。
///
/// 将整个 vault 余额退还给原始 制造商,
/// 有效地取消托管并清理帐户。
pub fn refund_and_close_vault(&mut self) -> Result<()> {
// Create PDA signer seeds for vault authority
// 为 vault 授权机构创建 PDA 签名者种子
let signer_seeds: &[&[&[u8]]; 1] = &[&[\
b"escrow",\
self.maker.key.as_ref(),\
&self.escrow.seed.to_le_bytes()[..],\
&[self.escrow.bump]\
]];
let decimals = self.mint_a.decimals;
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked{
mint: self.mint_a.to_account_info(),
from: self.vault.to_account_info(),
to: self.maker_ata_a.to_account_info(),
authority: self.escrow.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
// Return all escrowed tokens to maker
// 将所有托管的代币退还给 制造商
transfer_checked(cpi_context, self.vault.amount, decimals)?;
// Close vault account and refund rent to maker
// 关闭 vault 帐户并将租金退还给 制造商
let close_accounts = CloseAccount {
account: self.vault.to_account_info(),
destination: self.maker.to_account_info(),
authority: self.escrow.to_account_info(),
};
let close_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
close_accounts,
signer_seeds
);
close_account(close_cpi_ctx)?;
Ok(())
}
}
要点:
创建 programs/escrow/src/instructions/mod.rs
:
pub mod make;
pub mod take;
pub mod refund;
pub use make::*;
pub use take::*;
pub use refund::*;
现在让我们创建全面的测试。以下是基于你的实现的测试文件:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import * as spl from "@solana/spl-token";
import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram
} from "@solana/web3.js";
import { Escrow } from "../target/types/escrow";
describe("escrow", () => {
const provider = anchor.getProvider();
const program = anchor.workspace.escrow as Program<Escrow>;
const programId = program.programId;
const tokenProgram = spl.TOKEN_2022_PROGRAM_ID;
console.log("RPC:", provider.connection.rpcEndpoint);
const SEED = new anchor.BN(1);
const confirm = async (signature: string): Promise<string> => {
const block = await provider.connection.getLatestBlockhash();
await provider.connection.confirmTransaction({ signature, ...block });
return signature;
};
const log = async (signature: string): Promise<string> => {
console.log(
`Your transaction signature: https://explorer.solana.com/transaction/${signature}?cluster=devnet`
);
return signature;
};
const maker = Keypair.generate();
const taker = Keypair.generate();
const mintA = Keypair.generate();
const mintB = Keypair.generate();
const [makerAtaA, makerAtaB, takerAtaA, takerAtaB] = [maker, taker]
.map((a) =>
[mintA, mintB].map((m) =>
spl.getAssociatedTokenAddressSync(
m.publicKey,
a.publicKey,
false,
tokenProgram
)
)
)
.flat();
const escrow = PublicKey.findProgramAddressSync(
[\
Buffer.from("escrow"),\
maker.publicKey.toBuffer(),\
SEED.toArrayLike(Buffer, "le", 8),\
],
programId
)[0];
const vault = spl.getAssociatedTokenAddressSync(
mintA.publicKey,
escrow,
true,
tokenProgram
);
const accounts = {
maker: maker.publicKey,
taker: taker.publicKey,
mintA: mintA.publicKey,
mintB: mintB.publicKey,
makerAtaA,
makerAtaB,
takerAtaA,
takerAtaB,
escrow,
vault,
tokenProgram,
};
it("Airdrop & create mint", async () => {
const lamports = await spl.getMinimumBalanceForRentExemptMint(
provider.connection as any
);
const tx = new anchor.web3.Transaction();
// Transfer 1 SOL to maker and taker
// 将 1 SOL 转移到 制造商和交易接受者
tx.instructions.push(
...[maker, taker].map((a) =>
SystemProgram.transfer({
fromPubkey: provider.publicKey,
toPubkey: a.publicKey,
lamports: 1 * LAMPORTS_PER_SOL,
})
)
);
// Create mintA and mintB
// 创建 mintA 和 mintB
tx.instructions.push(
...[mintA, mintB].map((m) =>
SystemProgram.createAccount({
fromPubkey: provider.publicKey,
newAccountPubkey: m.publicKey,
lamports,
space: spl.MINT_SIZE,
programId: tokenProgram,
})
)
);
// Init mints, create ATAs, and mint tokens
// 初始化 铸币, 创建 ATAs, 和 铸币 tokens
tx.instructions.push(
...[\
{ mint: mintA.publicKey, authority: maker.publicKey, ata: makerAtaA },\
{ mint: mintB.publicKey, authority: taker.publicKey, ata: takerAtaB },\
].flatMap((x) => [\
spl.createInitializeMint2Instruction(\
x.mint,\
6,\
x.authority,\
null,\
tokenProgram\
),\
spl.createAssociatedTokenAccountIdempotentInstruction(\
provider.publicKey,\
x.ata,\
x.authority,\
x.mint,\
tokenProgram\
),\
spl.createMintToInstruction(\
x.mint,\
x.ata,\
x.authority,\
1e9,\
undefined,\
tokenProgram\
),\
])
);
await provider
.sendAndConfirm(tx, [provider.wallet.payer, maker, taker, mintA, mintB])
.then(log);
});
it("Make escrow", async () => {
await program.methods
.make(SEED, new anchor.BN(1e6), new anchor.BN(1e6))
.accounts({ ...accounts })
.signers([maker])
.rpc()
.then(confirm)
.then(log);
console.log("✅ Escrow created successfully");
});
it("Take escrow", async () => {
await program.methods
.take()
>- 原文链接: [blog.blockmagnates.com/e...](https://blog.blockmagnates.com/escrows-in-solana-6b74fdc17fa0)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!