发行类型

联合曲线兑换集成

Last updated April 9, 2026

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

本指南涵盖内容

本指南涵盖:

  • 获取并解析 BondingCurveBucketV2 账户状态
  • 使用 isSwappable、isSoldOut 和 isGraduated 检查生命周期状态
  • 使用 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触发时结束交易的条件。

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

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

五个辅助函数可在无需额外 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.Buy 或 SwapDirection.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——价格随每位用户的每次交易而变化
  • virtualSol 和 virtualTokens 在曲线创建后不可更改——可缓存;每次兑换只有真实储备字段变化
  • isGraduated 每次调用都会发起一次 RPC 请求——请在索引器中缓存结果
  • 在 isSoldOut 返回 true 与 isGraduated 返回 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)bigint将 amountOut 减少 slippageBps / 10_000

兑换指令账户

账户可写签名描述
genesisAccount是否Genesis 协调 PDA
bucket是否BondingCurveBucketV2 PDA
baseMint否否SPL 代币 mint
quoteMint否否wSOL 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 仅在曲线正在接受公开交易时返回 true。isSoldOut 在 baseTokenBalance 降至零的瞬间返回 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 字段。若实际输出低于此值,链上程序将拒绝交易。