发行类型

联合曲线兑换集成

Last updated April 9, 2026

使用 Genesis SDK 读取联合曲线状态、计算兑换报价、在链上执行买入和卖出交易、处理滑点以及认领创作者费。

本指南涵盖内容

本指南涵盖:

  • 获取并解析 BondingCurveBucketV2 账户状态
  • 使用 isSwappableisSoldOutisGraduated 检查生命周期状态
  • 使用 getSwapResult 获取精确的兑换报价
  • 使用 applySlippage 保护用户免受滑点影响
  • 使用 swapBondingCurveV2 构建买入和卖出交易
  • 认领曲线和毕业后 Raydium 池中的创作者费

摘要

联合曲线兑换使用 Genesis SDK 与链上 BondingCurveBucketV2 账户交互——这是一个恒积 AMM,接受 SOL 并返回代币(买入),或接受代币并返回 SOL(卖出)。有关定价数学基础,请参阅运作原理

  • 发送前先获取报价 — 调用 getSwapResult 获取精确的含手续费输入和输出金额
  • 滑点保护 — 使用 applySlippage 推导 minAmountOutScaled 并将其传给指令
  • wSOL 需手动处理 — 兑换指令不会自动包装或解包原生 SOL;调用方必须自行管理 wSOL ATA
  • Program ID — Solana 主网上为 GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B

快速开始

跳转至: 安装 · 配置 · 获取曲线 · 生命周期辅助函数 · 报价 · 滑点 · 执行兑换 · 创作者费 · 错误处理 · API 参考

  1. 安装依赖包并使用 genesis() 插件配置 Umi 实例
  2. 推导 BondingCurveBucketV2Pda 并获取账户
  3. 检查 isSwappable(bucket) — 若返回 false 则中止操作
  4. 调用 getSwapResult(bucket, amountIn, SwapDirection.Buy) 获取含手续费的报价
  5. 使用 applySlippage(quote.amountOut, slippageBps) 获得 minAmountOutScaled
  6. 手动处理 wSOL 包装,然后发送 swapBondingCurveV2 并确认

前置条件

  • Node.js 18+ — 原生 BigInt 支持所必需
  • Solana 钱包 — 已充值 SOL,用于支付交易费用和兑换输入
  • Solana RPC 端点(mainnet-beta 或 devnet)
  • 熟悉 Umi 框架 和 async/await 模式

测试配置

工具版本
@metaplex-foundation/genesis1.x
@metaplex-foundation/umi1.x
@metaplex-foundation/umi-bundle-defaults1.x
Node.js18+

安装

Terminal
npm install @metaplex-foundation/genesis \
@metaplex-foundation/umi \
@metaplex-foundation/umi-bundle-defaults

Umi 和 Genesis 插件配置

在调用任何 SDK 函数前,先配置 Umi 实例并注册 genesis() 插件。

setup.ts
1import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
2import { genesis } from '@metaplex-foundation/genesis';
3import { keypairIdentity } from '@metaplex-foundation/umi';
4import { readFileSync } from 'fs';
5
6const keypairFile = JSON.parse(readFileSync('/path/to/keypair.json', 'utf-8'));
7
8const umi = createUmi('https://api.mainnet-beta.solana.com')
9 .use(genesis());
10
11const keypair = umi.eddsa.createKeypairFromSecretKey(Uint8Array.from(keypairFile));
12umi.use(keypairIdentity(keypair));

获取 Bonding Curve BucketV2

根据已掌握的信息,可选用三种发现策略。

从已知 Genesis 账户获取

1import {
2 findBondingCurveBucketV2Pda,
3 fetchBondingCurveBucketV2,
4 genesis,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
8
9const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
10
11const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
12
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
14 genesisAccount,
15 bucketIndex: 0,
16});
17
18const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

从代币 Mint 获取

fetch-from-mint.ts
1import {
2 findGenesisAccountV2Pda,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7
8const baseMint = publicKey('TOKEN_MINT_PUBKEY');
9
10const [genesisAccount] = findGenesisAccountV2Pda(umi, {
11 baseMint,
12 genesisIndex: 0,
13});
14
15const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
16 genesisAccount,
17 bucketIndex: 0,
18});
19
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

读取 Bonding Curve BucketV2 状态

字段类型描述
baseTokenBalancebigint曲线上剩余的代币数量。零表示已售罄。
baseTokenAllocationbigint创建时分配给此曲线的代币总量。
quoteTokenDepositTotalbigint买家存入的真实 SOL 金额(lamports)。初始为 0。
virtualSolbigint初始化时添加的虚拟 SOL 储备(仅用于定价)。
virtualTokensbigint初始化时添加的虚拟代币储备(仅用于定价)。
depositFeenumber适用于每次兑换 SOL 侧的协议手续费率。
withdrawFeenumber适用于卖出 SOL 输出侧的协议手续费率。
creatorFeeAccruedbigint自上次认领以来累积的创作者费(lamports)。
creatorFeeClaimedbigint迄今累计认领的创作者费(lamports)。
swapStartConditionobject允许交易前必须满足的条件。
swapEndConditionobject触发时结束交易的条件。

virtualSolvirtualTokens 仅存在于定价数学中——它们从未作为真实资产存入链上。请参阅运作原理了解虚拟储备如何塑造恒积曲线。

联合曲线生命周期辅助函数

五个辅助函数可在无需额外 RPC 调用的情况下检查曲线状态(isGraduated 除外)。

lifecycle-helpers.ts
1import {
2 isSwappable,
3 isFirstBuyPending,
4 isSoldOut,
5 getFillPercentage,
6 isGraduated,
7} from '@metaplex-foundation/genesis';
8
9const canSwap = isSwappable(bucket);
10const firstBuyPending = isFirstBuyPending(bucket);
11const soldOut = isSoldOut(bucket);
12const fillPercent = getFillPercentage(bucket);
13const graduated = await isGraduated(umi, bucket); // async RPC call
辅助函数异步返回值描述
isSwappable(bucket)boolean接受公开交易时为 true
isFirstBuyPending(bucket)boolean指定首次购买尚未完成时为 true
isSoldOut(bucket)booleanbaseTokenBalance === 0n 时为 true
getFillPercentage(bucket)number已售出分配量的 0–100 百分比
isGraduated(umi, bucket)booleanRaydium CPMM 池链上存在时为 true

获取兑换报价

getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?) 无需发送任何交易即可计算兑换的精确含手续费金额。

返回 { amountIn, fee, creatorFee, amountOut }

  • amountIn — 经调整后的实际输入金额
  • fee — 收取的协议手续费,以 lamports 计
  • creatorFee — 收取的创作者费,以 lamports 计(未配置创作者费时为 0)
  • amountOut — 收到的代币(买入)或 SOL(卖出)

买入报价(SOL 换代币)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
17
18const buyQuote = getSwapResult(bucket, SOL_IN, 'buy');
19
20console.log('SOL input: ', buyQuote.amountIn.toString(), 'lamports');
21console.log('Total fee: ', buyQuote.fee.toString(), 'lamports');
22console.log('Tokens out: ', buyQuote.amountOut.toString());
23
24// SOL input: 1000000000 lamports
25// Total fee: 10000000 lamports
26// Tokens out: <calculated token amount>

卖出报价(代币换 SOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
17
18const sellQuote = getSwapResult(bucket, TOKENS_IN, 'sell');
19
20console.log('Tokens input: ', sellQuote.amountIn.toString());
21console.log('Total fee: ', sellQuote.fee.toString(), 'lamports');
22console.log('SOL out: ', sellQuote.amountOut.toString(), 'lamports');
23
24// Tokens input: 500000000000
25// Total fee: <fee in lamports>
26// SOL out: <calculated SOL amount>

首次购买手续费豁免

true 作为第四个参数传入,可以模拟免手续费的首次购买报价:

first-buy-quote.ts
1const firstBuyQuote = getSwapResult(bucket, SOL_IN, SwapDirection.Buy, true);
2console.log('Fee (waived): ', firstBuyQuote.fee.toString()); // 0n

当前价格辅助函数

current-price.ts
1import {
2 getCurrentPrice,
3 getCurrentPriceQuotePerBase,
4 getCurrentPriceComponents,
5} from '@metaplex-foundation/genesis';
6
7const tokensPerSol = getCurrentPrice(bucket); // bigint
8const lamportsPerToken = getCurrentPriceQuotePerBase(bucket); // bigint
9const { baseReserves, quoteReserves } = getCurrentPriceComponents(bucket);

滑点保护

applySlippage(expectedAmountOut, slippageBps) 将预期输出减去滑点容忍度。将结果作为 minAmountOutScaled 传给兑换指令——若实际输出低于此值,链上程序将拒绝交易。

slippage.ts
1import { getSwapResult, applySlippage, SwapDirection } from '@metaplex-foundation/genesis';
2
3const quote = getSwapResult(bucket, 1_000_000_000n, SwapDirection.Buy);
4const minAmountOutScaled = applySlippage(quote.amountOut, 100); // 1% slippage

切勿在未通过 applySlippage 推导 minAmountOutScaled 的情况下发送兑换交易。联合曲线的价格随每次交易变化;若不加滑点保护,用户可能收到远少于报价的代币。

常用值:稳定行情下 50 bps(0.5%),波动性发行时 200 bps(2%)。

构建兑换交易

swapBondingCurveV2(umi, accounts) 构建兑换指令。调用方负责在交易前后处理包装 SOL(wSOL)。

买入交易(SOL 换代币)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8 isSwappable,
9} from '@metaplex-foundation/genesis';
10import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
11import { publicKey } from '@metaplex-foundation/umi';
12import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
13
14const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
15
16const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
17const baseMint = publicKey('TOKEN_MINT_PUBKEY');
18const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
19
20const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
21const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
22
23if (!isSwappable(bucket)) throw new Error('Curve is not currently accepting swaps');
24
25const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
26const quote = getSwapResult(bucket, SOL_IN, 'buy');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
30const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
31
32// NOTE: Fund the wSOL ATA before this call — see the wSOL Wrapping Note on this page.
33const result = await swapBondingCurveV2(umi, {
34 genesisAccount,
35 bucketPda,
36 baseMint,
37 quoteMint,
38 userBaseTokenAccount,
39 userQuoteTokenAccount,
40 amountIn: quote.amountIn,
41 minAmountOut,
42 direction: 'buy',
43}).sendAndConfirm(umi);
44
45console.log('Buy confirmed:', result.signature);
46
47// Buy confirmed: <base58 transaction signature>

卖出交易(代币换 SOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8} from '@metaplex-foundation/genesis';
9import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
10import { publicKey } from '@metaplex-foundation/umi';
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
12
13const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
14
15const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
16const baseMint = publicKey('TOKEN_MINT_PUBKEY');
17const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
18
19const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
21
22const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
23const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
24
25const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
26const quote = getSwapResult(bucket, TOKENS_IN, 'sell');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const result = await swapBondingCurveV2(umi, {
30 genesisAccount,
31 bucketPda,
32 baseMint,
33 quoteMint,
34 userBaseTokenAccount,
35 userQuoteTokenAccount,
36 amountIn: quote.amountIn,
37 minAmountOut,
38 direction: 'sell',
39}).sendAndConfirm(umi);
40
41// NOTE: Close the wSOL ATA after confirming to unwrap back to native SOL.
42console.log('Sell confirmed:', result.signature);
43
44// Sell confirmed: <base58 transaction signature>

wSOL 包装说明

需手动处理 wSOL

swapBondingCurveV2 使用包装 SOL(wSOL)作为报价代币,不会自动包装或解包原生 SOL。

买入时: 创建 wSOL ATA,将所需 lamports 转入其中,并在发送兑换前调用 syncNative

卖出时: 兑换确认后关闭 wSOL ATA,将 wSOL 解包回原生 SOL。

当前版本仅接受 wSOL 作为报价代币。

wsol-wrap-unwrap.ts
1import {
2 findAssociatedTokenPda,
3 createAssociatedTokenAccountIdempotentInstruction,
4 syncNative,
5 closeToken,
6} from '@metaplex-foundation/mpl-toolbox';
7import { transactionBuilder, sol, publicKey } from '@metaplex-foundation/umi';
8
9const wSOL = publicKey('So11111111111111111111111111111111111111112');
10const [wSolAta] = findAssociatedTokenPda(umi, { mint: wSOL, owner: umi.identity.publicKey });
11
12// --- Wrap SOL before a buy ---
13const wrapBuilder = transactionBuilder()
14 .add(createAssociatedTokenAccountIdempotentInstruction(umi, {
15 mint: wSOL,
16 owner: umi.identity.publicKey,
17 }))
18 .add(syncNative(umi, { account: wSolAta }));
19
20await wrapBuilder.sendAndConfirm(umi);
21
22// --- Unwrap SOL after a sell ---
23const unwrapBuilder = closeToken(umi, {
24 account: wSolAta,
25 destination: umi.identity.publicKey,
26 authority: umi.identity,
27});
28
29await unwrapBuilder.sendAndConfirm(umi);

认领创作者费

创作者费在每次兑换时累积到 bucket(creatorFeeAccrued),而非直接转账。通过无需许可的 claimBondingCurveCreatorFeeV2 指令在曲线活跃时收取,并通过 claimRaydiumCreatorFeeV2 在毕业后收取。

完整认领流程——包括如何检查累积余额以及处理毕业后 Raydium LP 费用——请参阅创作者费

错误处理

错误原因解决方法
BondingCurveInsufficientFunds曲线持有的代币(买入)或 SOL(卖出)不足重新获取 bucket 并重新报价;曲线可能即将售罄
InsufficientOutputAmount实际输出低于 minAmountOutScaled增大 slippageBps 或立即重试
InvalidSwapDirectionswapDirection 值无效@metaplex-foundation/genesis 导入传入 SwapDirection.BuySwapDirection.Sell
BondingCurveNotStartedswapStartCondition 尚未满足检查 bucket.swapStartCondition 并等待
BondingCurveEnded曲线已售罄或已毕业将用户引导至 Raydium CPMM 池
error-handling.ts
1async function executeBuy(bucket, amountIn: bigint, slippageBps: number) {
2 if (!isSwappable(bucket)) {
3 if (isSoldOut(bucket)) throw new Error('Token sold out. Trade on Raydium.');
4 throw new Error('Curve not yet active. Check the start time.');
5 }
6
7 const quote = getSwapResult(bucket, amountIn, SwapDirection.Buy);
8 const minAmountOutScaled = applySlippage(quote.amountOut, slippageBps);
9
10 try {
11 return await swapBondingCurveV2(umi, {
12 amount: quote.amountIn,
13 minAmountOutScaled,
14 swapDirection: SwapDirection.Buy,
15 // ... accounts
16 }).sendAndConfirm(umi);
17 } catch (err: any) {
18 if (err.message?.includes('InsufficientOutputAmount'))
19 throw new Error('Price moved. Try again with higher slippage.');
20 if (err.message?.includes('BondingCurveInsufficientFunds'))
21 throw new Error('Not enough tokens remaining. Reduce amount.');
22 throw err;
23 }
24}

注意事项

  • 在生产环境中,每次兑换前都应重新获取 bucket——价格随每位用户的每次交易而变化
  • virtualSolvirtualTokens 在曲线创建后不可更改——可缓存;每次兑换只有真实储备字段变化
  • isGraduated 每次调用都会发起一次 RPC 请求——请在索引器中缓存结果
  • isSoldOut 返回 trueisGraduated 返回 true 之间,曲线已售罄但 Raydium 尚未注入资金;在 isGraduated 确认前不要将用户引导至 Raydium
  • 事件解码和生命周期索引,请参阅索引与事件
  • 所有手续费金额以 lamports 计(SOL 侧);当前费率请参阅协议费用

API 参考

报价和价格函数

函数异步返回值描述
getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?){ amountIn, fee, creatorFee, amountOut }含手续费的兑换报价
getCurrentPrice(bucket)bigint每 SOL 单位可换基础代币数(整数除法)
getCurrentPriceQuotePerBase(bucket)bigint每个基础代币单位所需 lamports(整数除法)
getCurrentPriceComponents(bucket){ baseReserves, quoteReserves }虚拟+真实组合储备(bigint)

生命周期函数

函数异步返回值描述
isSwappable(bucket)boolean接受公开交易时为 true
isFirstBuyPending(bucket)boolean指定首次购买尚未完成时为 true
isSoldOut(bucket)booleanbaseTokenBalance === 0n 时为 true
getFillPercentage(bucket)number已售出分配量的 0–100 百分比
isGraduated(umi, bucket)booleanRaydium CPMM 池链上存在时为 true

滑点

函数返回值描述
applySlippage(amountOut, slippageBps)bigintamountOut 减少 slippageBps / 10_000

兑换指令账户

账户可写签名描述
genesisAccountGenesis 协调 PDA
bucketBondingCurveBucketV2 PDA
baseMintSPL 代币 mint
quoteMintwSOL mint
baseTokenAccount用户的基础代币 ATA
quoteTokenAccount用户的 wSOL ATA
payer交易手续费付款方

账户发现

函数返回值描述
findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex })[PublicKey, bump]推导 bucket PDA
findGenesisAccountV2Pda(umi, { baseMint, genesisIndex })[PublicKey, bump]推导 genesis 账户 PDA
fetchBondingCurveBucketV2(umi, pda)BondingCurveBucketV2获取并反序列化账户

FAQ

isSwappable 和 isSoldOut 有什么区别?

isSwappable 仅在曲线正在接受公开交易时返回 trueisSoldOutbaseTokenBalance 降至零的瞬间返回 true,此时交易结束并触发毕业流程。曲线可以已售罄但尚未毕业。

调用 swapBondingCurveV2 之前需要先包装 SOL 吗?

是的。联合曲线使用 wSOL 作为报价代币,swapBondingCurveV2 不会自动包装或解包原生 SOL。详见 wSOL 包装说明

getSwapResult 返回什么,它如何处理手续费?

getSwapResult 返回 { amountIn, fee, creatorFee, amountOut }。买入时,手续费在 AMM 公式运算前从 SOL 输入中扣除。卖出时,手续费在 AMM 公式运算后从 SOL 输出中扣除。将 true 作为第四个参数传入以模拟首次购买手续费豁免(所有手续费归零)。

如何防止滑点?

调用 applySlippage(quote.amountOut, slippageBps) 推导 minAmountOutScaled,然后将其传给 swapBondingCurveV2 作为 minAmountOutScaled 字段。若实际输出低于此值,链上程序将拒绝交易。