发行类型

Presale

Last updated January 31, 2026

Presale提供 Solana 上的固定价格代币分配。根据分配量和 SOL 上限预先设定 SPL 代币价格——用户确切知道会得到什么,您也确切知道会筹集多少。在 Genesis 中,"Presale"指的是在初始交易之前直接出售代币——购买者直接收到代币,而非未来收到代币的权利。

学习内容

本指南涵盖:

  • Presale定价如何运作(分配量 + 上限 = 价格)
  • 设置存款窗口和领取期间
  • 配置存款限额和冷却时间
  • 用户操作:包装SOL、存款和领取

概述

Presale以预定价格出售代币,类似于传统的代币发售。价格根据您配置的代币分配量和SOL上限计算,非常适合已知估值的加密融资。

  • 固定价格 = SOL上限 / 代币分配量
  • 用户在存款窗口期间存入SOL(收取2%费用)
  • 先到先得,直到SOL上限
  • 可选:最小/最大存款限额、冷却时间、后端授权

超出范围

自然价格发现(参见Launch Pool)、基于出价的拍卖(参见Uniform Price Auction)和归属计划。

工作原理

  1. 您将代币分配到Presale,设置SOL上限来决定固定价格
  2. 用户在存款窗口期间以固定汇率存入SOL
  3. 存款期结束后,您执行过渡以转移资金
  4. 用户根据存款金额领取代币

价格计算

代币价格由分配代币与SOL上限的比率决定:

price = allocationQuoteTokenCap / baseTokenAllocation
tokens = deposit / price

例如,如果您分配1,000,000代币,SOL上限为100:

  • 价格 = 100 SOL / 1,000,000代币 = 每代币0.0001 SOL
  • 存入10 SOL可获得100,000代币

费用

指令Solana
Deposit2%
Graduation5%

快速开始

设置指南

前提条件

npm install @metaplex-foundation/genesis @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-toolbox

1. 初始化Genesis Account

Genesis Account创建您的代币并协调所有分配Bucket。

initializeV2.ts
1import {
2 findGenesisAccountV2Pda,
3 genesis,
4 initializeV2,
5} from '@metaplex-foundation/genesis'
6import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
7import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
9
10const umi = createUmi('https://api.mainnet-beta.solana.com')
11 .use(mplToolbox())
12 .use(genesis())
13
14// umi.use(keypairIdentity(yourKeypair));
15
16const baseMint = generateSigner(umi)
17const TOTAL_SUPPLY = 1_000_000_000_000_000n // 1 million tokens (9 decimals)
18
19// Store this account address for later or recreate it when needed.
20const [genesisAccount] = findGenesisAccountV2Pda(umi, {
21 baseMint: baseMint.publicKey,
22 genesisIndex: 0,
23})
24
25await initializeV2(umi, {
26 baseMint,
27 fundingMode: 0,
28 totalSupplyBaseToken: TOTAL_SUPPLY,
29 name: 'My Token',
30 symbol: 'MTK',
31 uri: 'https://example.com/metadata.json',
32}).sendAndConfirm(umi)

totalSupplyBaseToken应该等于所有Bucket分配的总和。

2. 添加Presale Bucket

Presale Bucket收集存款并分配代币。在此配置时间和可选限制。

addPresaleBucket.ts
1import {
2 genesis,
3 addPresaleBucketV2,
4 findPresaleBucketV2Pda,
5 findUnlockedBucketV2Pda,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.
18
19const [presaleBucket] = findPresaleBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
20const [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
21
22const now = BigInt(Math.floor(Date.now() / 1000))
23const depositStart = now
24const depositEnd = now + 86400n // 24 hours
25const claimStart = depositEnd + 1n
26const claimEnd = claimStart + 604800n // 1 week
27
28await addPresaleBucketV2(umi, {
29 genesisAccount,
30 baseMint: baseMint.publicKey,
31 baseTokenAllocation: TOTAL_SUPPLY,
32 allocationQuoteTokenCap: 100_000_000_000n, // 100 SOL cap (sets price)
33
34 // Timing
35 depositStartCondition: {
36 __kind: 'TimeAbsolute',
37 padding: Array(47).fill(0),
38 time: depositStart,
39 triggeredTimestamp: null,
40 },
41 depositEndCondition: {
42 __kind: 'TimeAbsolute',
43 padding: Array(47).fill(0),
44 time: depositEnd,
45 triggeredTimestamp: null,
46 },
47 claimStartCondition: {
48 __kind: 'TimeAbsolute',
49 padding: Array(47).fill(0),
50 time: claimStart,
51 triggeredTimestamp: null,
52 },
53 claimEndCondition: {
54 __kind: 'TimeAbsolute',
55 padding: Array(47).fill(0),
56 time: claimEnd,
57 triggeredTimestamp: null,
58 },
59
60 // Optional: Deposit limits
61 minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }
62 depositLimit: null, // or { limit: sol(10).basisPoints }
63
64 // Where collected SOL goes after transition
65 endBehaviors: [
66 {
67 __kind: 'SendQuoteTokenPercentage',
68 padding: Array(4).fill(0),
69 destinationBucket: publicKey(unlockedBucket),
70 percentageBps: 10000, // 100%
71 processed: false,
72 },
73 ],
74}).sendAndConfirm(umi)

3. 添加Unlocked Bucket

Unlocked Bucket在过渡后从Presale接收SOL。

addUnlockedBucket.ts
1import {
2 addUnlockedBucketV2,
3 genesis,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, baseMint, claimStart, and claimEnd from previous steps.
16
17await addUnlockedBucketV2(umi, {
18 genesisAccount,
19 baseMint: baseMint.publicKey,
20 baseTokenAllocation: 0n,
21 recipient: umi.identity.publicKey,
22 claimStartCondition: {
23 __kind: 'TimeAbsolute',
24 padding: Array(47).fill(0),
25 time: claimStart,
26 triggeredTimestamp: null,
27 },
28 claimEndCondition: {
29 __kind: 'TimeAbsolute',
30 padding: Array(47).fill(0),
31 time: claimEnd,
32 triggeredTimestamp: null,
33 },
34 backendSigner: null,
35}).sendAndConfirm(umi)

4. Finalize

所有Bucket配置完成后,Finalize以激活预售。这是不可逆的。

finalize.ts
1import {
2 genesis,
3 finalizeV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount and baseMint from the Initialize step.
16
17await finalizeV2(umi, {
18 baseMint: baseMint.publicKey,
19 genesisAccount,
20}).sendAndConfirm(umi)

用户操作

包装SOL

用户必须在存款前将SOL包装为wSOL。

wrapSol.ts
1import {
2 findAssociatedTokenPda,
3 createTokenIfMissing,
4 transferSol,
5 syncNative,
6 mplToolbox,
7} from '@metaplex-foundation/mpl-toolbox'
8import { WRAPPED_SOL_MINT, genesis } from '@metaplex-foundation/genesis'
9import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
10import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
11
12const umi = createUmi('https://api.mainnet-beta.solana.com')
13 .use(mplToolbox())
14 .use(genesis())
15
16// umi.use(keypairIdentity(yourKeypair));
17
18const userWsolAccount = findAssociatedTokenPda(umi, {
19 owner: umi.identity.publicKey,
20 mint: WRAPPED_SOL_MINT,
21})
22
23await createTokenIfMissing(umi, {
24 mint: WRAPPED_SOL_MINT,
25 owner: umi.identity.publicKey,
26 token: userWsolAccount,
27})
28 .add(
29 transferSol(umi, {
30 destination: publicKey(userWsolAccount),
31 amount: sol(10),
32 })
33 )
34 .add(syncNative(umi, { account: userWsolAccount }))
35 .sendAndConfirm(umi)

存款

depositPresale.ts
1import {
2 genesis,
3 depositPresaleV2,
4 findPresaleDepositV2Pda,
5 fetchPresaleDepositV2,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
18
19await depositPresaleV2(umi, {
20 genesisAccount,
21 bucket: presaleBucket,
22 baseMint: baseMint.publicKey,
23 amountQuoteToken: sol(1).basisPoints,
24}).sendAndConfirm(umi)
25
26// Verify
27const [depositPda] = findPresaleDepositV2Pda(umi, {
28 bucket: presaleBucket,
29 recipient: umi.identity.publicKey,
30})
31const deposit = await fetchPresaleDepositV2(umi, depositPda)
32
33console.log('Deposited (after fee):', deposit.amountQuoteToken)

同一用户的多次存款会累积到单个存款账户中。

领取代币

存款期结束并开始领取后:

claimPresale.ts
1import {
2 genesis,
3 claimPresaleV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
16
17await claimPresaleV2(umi, {
18 genesisAccount,
19 bucket: presaleBucket,
20 baseMint: baseMint.publicKey,
21 recipient: umi.identity.publicKey,
22}).sendAndConfirm(umi)

代币分配:userTokens = (userDeposit / allocationQuoteTokenCap) * baseTokenAllocation

管理员操作

执行过渡

存款结束后,执行过渡将收集的SOL转移到Unlocked Bucket。

transitionPresale.ts
1import {
2 genesis,
3 transitionV2,
4 WRAPPED_SOL_MINT,
5} from '@metaplex-foundation/genesis'
6import {
7 findAssociatedTokenPda,
8 mplToolbox,
9} from '@metaplex-foundation/mpl-toolbox'
10import { keypairIdentity } from '@metaplex-foundation/umi'
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
12
13const umi = createUmi('https://api.mainnet-beta.solana.com')
14 .use(mplToolbox())
15 .use(genesis())
16
17// umi.use(keypairIdentity(yourKeypair));
18
19// Assumes genesisAccount, presaleBucket, unlockedBucket, and baseMint from previous steps.
20
21const [unlockedBucketQuoteTokenAccount] = findAssociatedTokenPda(umi, {
22 owner: unlockedBucket,
23 mint: WRAPPED_SOL_MINT,
24})
25
26await transitionV2(umi, {
27 genesisAccount,
28 primaryBucket: presaleBucket,
29 baseMint: baseMint.publicKey,
30})
31 .addRemainingAccounts([
32 { pubkey: unlockedBucket, isSigner: false, isWritable: true },
33 { pubkey: unlockedBucketQuoteTokenAccount, isSigner: false, isWritable: true },
34 ])
35 .sendAndConfirm(umi)

为什么这很重要: 没有过渡,收集的SOL将保持锁定在Presale Bucket中。用户仍然可以领取代币,但团队无法访问筹集的资金。

参考

配置选项

这些选项在创建Presale Bucket时设置:

选项描述示例
minimumDepositAmount每笔交易最小存款{ amount: sol(0.1).basisPoints }
depositLimit每用户最大总存款{ limit: sol(10).basisPoints }
depositCooldown存款之间的等待时间{ seconds: 60n }
perCooldownDepositLimit每个冷却期最大存款{ amount: sol(1).basisPoints }

时间条件

四个条件控制Presale时间:

条件用途
depositStartCondition存款开始时间
depositEndCondition存款结束时间
claimStartCondition领取开始时间
claimEndCondition领取结束时间

使用Unix时间戳的TimeAbsolute

const condition = {
__kind: 'TimeAbsolute',
padding: Array(47).fill(0),
time: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1小时后
triggeredTimestamp: null,
};

结束行为

定义存款期后收集的SOL如何处理:

endBehaviors: [
{
__kind: 'SendQuoteTokenPercentage',
padding: Array(4).fill(0),
destinationBucket: publicKey(unlockedBucket),
percentageBps: 10000, // 100% = 10000基点
processed: false,
},
]

获取状态

Bucket状态:

import { fetchPresaleBucketV2 } from '@metaplex-foundation/genesis';
const bucket = await fetchPresaleBucketV2(umi, presaleBucket);
console.log('Total deposits:', bucket.quoteTokenDepositTotal);
console.log('Deposit count:', bucket.depositCount);
console.log('Token allocation:', bucket.bucket.baseTokenAllocation);
console.log('SOL cap:', bucket.allocationQuoteTokenCap);

存款状态:

import { fetchPresaleDepositV2, safeFetchPresaleDepositV2 } from '@metaplex-foundation/genesis';
const deposit = await fetchPresaleDepositV2(umi, depositPda); // 未找到则抛出错误
const maybeDeposit = await safeFetchPresaleDepositV2(umi, depositPda); // 返回null
if (deposit) {
console.log('Amount deposited:', deposit.amountQuoteToken);
console.log('Amount claimed:', deposit.amountClaimed);
console.log('Fully claimed:', deposit.claimed);
}

注意事项

  • 存款需支付2%协议费
  • 用户必须在存款前将SOL包装为wSOL
  • 同一用户的多次存款累积在一个存款账户中
  • 存款结束后必须执行过渡,团队才能访问资金
  • Finalize是永久性的——在调用finalizeV2之前请仔细检查所有配置

常见问题

Presale中代币价格如何计算?

价格等于SOL上限除以代币分配量。对于100 SOL上限和1,000,000代币,价格为每代币0.0001 SOL。

如果没有达到SOL上限会怎样?

用户仍按存款比例获得代币。如果对100 SOL上限只存入了50 SOL,存款人将获得分配代币的50%。

可以设置每用户存款限额吗?

可以。使用minimumDepositAmount设置每笔交易的最低限额,使用depositLimit设置每用户的最大存款总额。

Presale和Launch Pool有什么区别?

Presale的价格由代币分配量和SOL上限固定。Launch Pool根据总存款量自然发现价格。

什么时候应该使用Presale vs Launch Pool?

当您需要可预测的定价并明确知道要筹集多少时使用Presale。使用Launch Pool进行自然价格发现。

术语表

术语定义
Presale预定汇率的固定价格代币销售
SOL CapPresale接受的最大SOL(决定价格)
Token AllocationPresale中可用的代币数量
Deposit Limit每用户允许的最大总存款
Minimum Deposit每笔存款交易所需的最小金额
Cooldown用户在存款之间必须等待的时间
End Behavior存款期结束后的自动化操作
Transition处理结束行为的指令

下一步