Solana[part19]_ETF的介绍和创建指令实现

Solana[part19]_ETF的介绍和创建指令实现
SoniaChenETF的介绍和创建指令实现

代码提交地址:https://github.com/SoniaChan33/iswap/commit/25b017929923a3d1e46d853f3a12da05f0b77172
一、项目概述
iSwap项目中的ETF创建功能用于在Solana区块链上生成自定义ETF(交易所交易基金),核心目标是实现ETF相关账户的创建与管理,包括ETF信息存储账户、资产关联账户(ATA)、ETF代币铸造账户(Mint)及元数据(Metadata)账户,确保所有操作的原子性与链上数据一致性。
二、核心链上组件(Rust程序)
链上逻辑通过Solana程序(智能合约)实现,负责ETF相关账户的初始化、数据存储及跨程序调用(CPI)。
1. 账户结构定义(etf_token.rs)
定义了链上存储ETF数据的核心账户结构,用于持久化ETF的关键信息。
EtfToken 账户
存储ETF的整体信息,包括铸造账户地址、创建者、创建时间、描述及资产列表:
#[account]
#[derive(InitSpace)]
pub struct EtfToken {
pub mint_account: Pubkey, // ETF代币的Mint账户地址
pub creator: Pubkey, // ETF创建者地址
pub create_at: i64, // 创建时间(Unix时间戳)
#[max_len(50)]
pub descriptor: String, // ETF描述信息(最大长度50)
#[max_len(10)]
pub assets: Vec<EtfAsset>, // 包含的资产列表(最多10个资产)
}
- 种子前缀:
SEEDS_PREFIX = "ETF_TOKEN"(用于PDA派生) - 初始化方法:
new方法接收参数并返回实例,自动填充创建时间(通过Clock获取)。
EtfAsset 账户
存储ETF中单个资产的信息(代币地址及权重):
#[account]
#[derive(InitSpace)]
pub struct EtfAsset {
pub token: Pubkey, // 资产代币的Mint地址
pub weight: u16, // 资产在ETF中的权重(数值类型为无符号16位整数)
}
- 初始化方法:
new方法接收代币地址和权重,返回实例。
2. ETF创建指令(etf_create.rs)
实现ETF创建的核心逻辑,包括账户初始化、元数据创建及数据存储。
输入参数 EtfTokenArgs
客户端传入的ETF配置信息,用于初始化链上数据:
#[account]
pub struct EtfTokenArgs {
pub name: String, // ETF名称
pub symbol: String, // ETF符号(唯一标识,用于派生地址)
pub description: String, // ETF描述
pub url: String, // ETF元数据URL(如官网、详情页)
pub assets: Vec<EtfAsset>, // 包含的资产列表
}
账户约束 EtfTokenCreate
定义指令所需的账户及约束条件,确保账户合法性:
#[derive(Accounts)]
#[instruction(args: EtfTokenArgs)]
pub struct EtfTokenCreate<'info> {
// ETF信息账户(PDA),若不存在则初始化
#[account(
init_if_needed,
payer = authority,
space = 8 + EtfToken::INIT_SPACE, // 8字节为账户 discriminator
seeds = [EtfToken::SEEDS_PREFIX.as_bytes(), etf_token_mint_account.key().as_ref()],
bump
)]
pub etf_token_info: Account<'info, EtfToken>,
// ETF元数据账户(关联Mint,通过元数据程序规则派生)
#[account(
mut,
seeds = [b"metadata", token_metadata_program.key().as_ref(), etf_token_mint_account.key().as_ref()],
bump,
seeds::program = token_metadata_program.key()
)]
pub etf_metadata_account: UncheckedAccount<'info>,
// ETF代币Mint账户(PDA),若不存在则初始化
#[account(
init_if_needed,
payer = authority,
seeds = [EtfToken::SEEDS_PREFIX.as_bytes(), args.symbol.as_bytes()],
bump,
mint::decimals = 9, // 固定小数位为9
mint::authority = etf_token_info.key() // 权限归属于ETF信息账户
)]
pub etf_token_mint_account: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>, // 交易签名者(创建者钱包)
pub rent: Sysvar<'info, Rent>, // Solana租金系统变量
pub token_program: Program<'info, Token>, // SPL Token程序
pub system_program: Program<'info, System>, // 系统程序
pub token_metadata_program: Program<'info, Metadata>, // MPL元数据程序
}
指令逻辑 etf_token_create
- 派生PDA签名种子:使用
etf_token_info的种子和bump生成签名种子(用于CPI授权)。 - 创建元数据账户:通过CPI调用MPL元数据程序,创建关联ETF Mint的元数据账户,包含名称、符号、URL等信息。
- 初始化ETF信息账户:将
EtfTokenArgs转换为EtfToken实例,存储创建者、Mint地址、时间等数据。
三、客户端核心逻辑(TypeScript)
客户端负责构建交易、派生地址、处理账户交互,最终调用链上程序完成ETF创建。
1. 基础配置(const.ts)
提供程序实例、Provider及默认钱包,作为客户端与链上交互的基础:
import * as anchor from "@coral-xyz/anchor";
import { Iswap } from "../../target/types/iswap";
// 初始化Provider(连接Solana网络)
let provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// 程序实例(关联iSwap程序)
const program = anchor.workspace.Iswap as anchor.Program<Iswap>;
// 默认钱包(本地钱包)
export function useDefaultWallet() {
const wallet = anchor.Wallet.local();
return wallet;
}
export { program, provider };
2. 地址派生(address.ts)
通过种子规则派生ETF相关账户的PDA地址,确保客户端与链上程序地址一致。
deriveEtfTokenMintAccount
根据ETF符号(symbol)派生Mint账户地址:
export function deriveEtfTokenMintAccount(symbol: string) {
return anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("etf_token"), Buffer.from(symbol)], // 种子:前缀 + symbol
program.programId
);
}
deriveEtfInfoAccount
根据Mint账户地址派生ETF信息账户(etf_token_info)地址:
export function deriveEtfInfoAccount(symbol: string) {
const [mintAccount,] = deriveEtfTokenMintAccount(symbol);
return anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("etf_token"), mintAccount.toBuffer()], // 种子:前缀 + Mint地址
program.programId
);
}
3. ETF创建函数(etf_token.ts)
createETF函数是客户端创建ETF的入口,负责构建交易并发送至链上:
核心流程:
- 派生账户地址:通过
deriveEtfInfoAccount获取etf_token_info账户地址。 - 创建资产ATA账户:为ETF包含的每个资产创建关联Token账户(ATA),若账户不存在则添加创建指令(确保ETF能持有资产)。
- 构建交易:
- 添加ATA创建指令(若需要)。
- 添加调用链上
etfCreate方法的指令(传入名称、符号、资产等参数)。
- 发送交易:使用Provider发送交易,由创建者钱包签名。
export async function createETF(wallet: anchor.Wallet, name: string, symbol: string, description: string, url: string, assets: { token: PublicKey, weight: number }[]) {
// 派生ETF信息账户地址
const [etfTokenInfoAddress,] = deriveEtfInfoAccount(symbol);
// 初始化交易
let tx = new anchor.web3.Transaction();
// 为每个资产创建ATA(若不存在)
for (const { token } of assets) {
const address = await getAssociatedTokenAddressSync(token, etfTokenInfoAddress, true);
try {
await getAccount(provider.connection, address); // 检查账户是否存在
} catch (e) {
if (e instanceof TokenAccountNotFoundError) {
// 添加创建ATA的指令
tx.add(createAssociatedTokenAccountInstruction(
wallet.payer.publicKey, // payer
address, // ATA地址
etfTokenInfoAddress, // 所有者(ETF信息账户)
token // 资产代币Mint
));
}
}
}
// 添加调用链上程序的指令
tx.add(await program.methods.etfCreate({
name, symbol, description, url, assets
}).transaction());
// 发送并确认交易
return await anchor.web3.sendAndConfirmTransaction(
provider.connection,
tx,
[wallet.payer] // 签名者
);
}
4. 示例调用(index.ts)
演示如何使用createETF函数创建ETF,包含参数示例:
(async () => {
const defaultWallet = useDefaultWallet(); // 使用默认钱包
// ETF基本信息
const [name, symbol, description, url] = [
"MyETF3",
"MYETF3",
"This is my ETF3",
"https://my-etf.com",
];
// 包含的资产(代币地址+权重)
const assets = [
{
token: new anchor.web3.PublicKey("FGNSbKTiKd4d1Zv97c2iQACADB8N7MnDnDsv5auHGAHo"),
weight: 90,
},
{
token: new anchor.web3.PublicKey("GTzKipZ6PcTEV2iV4LP1Yowwc4QPt1hvKmk6vuqY8ywp"),
weight: 10,
},
];
// 调用创建函数并打印交易哈希
const tx = await createETF(defaultWallet, name, symbol, description, url, assets);
console.log(tx);
})();
四、完整流程总结
- 客户端准备:用户指定ETF参数(名称、符号、资产等),通过
useDefaultWallet获取签名钱包。 - 地址派生:客户端通过
deriveEtfTokenMintAccount和deriveEtfInfoAccount计算Mint和信息账户的PDA地址。 - 交易构建:
- 检查并创建资产ATA账户(确保ETF可持有资产)。
- 构建调用链上
etfCreate指令的交易。
- 链上处理:
- 程序验证账户约束,初始化Mint账户(若不存在)。
- 通过CPI创建元数据账户,关联Mint。
- 初始化
etf_token_info账户,存储ETF数据。
- 交易确认:客户端接收交易哈希,完成ETF创建。
五、关键技术点
- PDA(Program Derived Address):用于生成Mint和信息账户地址,确保地址唯一性和程序控制权。
- ATA(Associated Token Account):为ETF自动创建资产持有账户,遵循SPL Token标准。
- CPI(Cross-Program Invocation):调用MPL元数据程序创建元数据,实现模块化功能复用。
- 原子性交易:所有操作(ATA创建、程序调用)在同一交易中完成,确保要么全部成功,要么全部失败。







