@masterpeach/launchpad-sdk 0.1.0-next.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh-CN.md CHANGED
@@ -1,12 +1,11 @@
1
- # MasterPeach Launchpad SDK
1
+ # @masterpeach/launchpad-sdk
2
2
 
3
- [English](./README.md)
3
+ 面向 ARC Launchpad **Production V2** 合约的 TypeScript SDK,仅依赖 Viem。一个 Launch 先在 Pons 式
4
+ bonding curve 上出售代币,卖到固定的 Token 侧边界后,通过两个可独立重试的无许可步骤毕业到 Uniswap V4 池。
4
5
 
5
- 面向 ARC Launchpad 的类型化 TypeScript SDK,仅依赖 Viem。覆盖部署解析与校验、生成式 ABI 绑定、
6
- 链上只读与报价、精确额度授权、先模拟后广播的写入管线,以及回执事件解码。
7
-
8
- SDK 唯一需要消费端提供的依赖是 `viem`。金额、网络、交易和错误等共享基础能力已内联,并从根入口
9
- 重新导出。SDK 不包含 React hooks、Wagmi adapter 或 Indexer 客户端 —— 这些属于应用层。
6
+ 本包只支持 Production V2。它**不是**上一版 V3 SDK 的升级:合约体系整个重写了,V3 的地址、ABI、类型
7
+ 没有任何一个能沿用。Referrer 系统、独立的 creator denylist 注册表、暂停开关、一步式迁移、WAD 定点曲线
8
+ 全部不复存在。
10
9
 
11
10
  ## 安装
12
11
 
@@ -14,229 +13,376 @@ SDK 唯一需要消费端提供的依赖是 `viem`。金额、网络、交易和
14
13
  pnpm add @masterpeach/launchpad-sdk viem
15
14
  ```
16
15
 
17
- ## 入口
16
+ ## 最容易踩的四个坑
17
+
18
+ 写任何对接代码之前先看这四条,每一条都会让调用**看起来对但实际失败**。
18
19
 
19
- | 导入路径 | 内容 |
20
- | --- | --- |
21
- | `@masterpeach/launchpad-sdk` | 客户端、actions、领域类型、部署、错误、纯函数 |
22
- | `@masterpeach/launchpad-sdk/abi` | 生成的 ABI 绑定与 artifact commit |
20
+ **1. 每个 Launch 有自己的 `BondingCurve`,交易发给 curve,不是 factory。** Factory 只负责创建和查询。
21
+ 用 `getCurveAddress`(或从 `TokenCreated` / `getLaunchRecord` 里取)解析出 curve 并按 token 缓存。
23
22
 
24
- 前端接入的完整说明见 [docs/integration-guide.md](./docs/integration-guide.md)
25
- ([English](./docs/integration-guide.en.md)),含 Wagmi adapter、query key 与迁移清单。
23
+ **2. 授权的 spender 因操作而异,而且 forwarder 地址换过。** 交易资金由 **curve** 拉取;直接
24
+ `createToken` 由 **factory** 拉取;走 forwarder 的两个入口都由 **forwarder** 自己拉取。forwarder
25
+ 地址请从 deployment 读,不要写死——2026-09-07 已轮换到 Holder-aware forwarder,授权给旧地址的额度
26
+ 不会自动转移。`prepareApprove` 要求显式传 `spender`;用
27
+ `launchpadCreationSpender(deployment, entryPoint)` 拿到正确的那个。
26
28
 
27
- ## 部署与 capability
29
+ **3. 开盘狙击税按交易的 token recipient 判定,不是付款人。** Router 代付时既不会借出也不会继承自己的
30
+ 豁免资格。调 `prepareBuy` 时要传真实的 recipient,否则会按错误的费率报价、并设出一个永远达不到的
31
+ `minTokenOut`。
28
32
 
29
- Arc 目前**只有测试网**,正式网尚未上线。因此内置的生产 registry 刻意保持为空,作为未来经核验
30
- manifest 的预留入口。Arc Testnet V2 以具名方式导出,必须显式接入。
33
+ **4. 一个 Launch 可能在别人的交易里毕业。** RC2 会在某笔买入(包括创建时的首购)吃掉最后一份可售
34
+ Token 时自动毕业。不要假设刚创建的 Launch 一定是 `notGraduated`,也不要不重新读状态就调 `graduate`。
35
+ SDK 会给这类交易显式设置 gas limit;如果你从 `request` 手工重建交易,请保留 `gas` 字段——只用估算值
36
+ 会让交易内的自动毕业 gas 不足、必然 revert。
31
37
 
32
- 类型化客户端由 **chain config** 构造,它同时携带经校验的 deployment **和**产品 capabilities。
33
- 配置只有一个真源,不存在第二处可漂移,写入门禁也无法被绕过。
38
+ ## 创建客户端
34
39
 
35
40
  ```ts
36
41
  import {
37
42
  arcTestnetLaunchpadDeployment,
38
- createLaunchpadPortalClient,
43
+ createLaunchpadFactoryClient,
39
44
  defineLaunchpadChain,
40
45
  verifyLaunchpadDeployment,
41
46
  } from "@masterpeach/launchpad-sdk";
42
47
 
48
+ // 另一个是 `arcMainnetLaunchpadDeployment`(chain 5042)。两者没有任何地址相同,contractVersion 也
49
+ // 不同;都不是注册的默认值,按名字选。
50
+ const deployment = arcTestnetLaunchpadDeployment;
51
+
43
52
  const chain = defineLaunchpadChain({
44
53
  slug: "arc-testnet",
45
- chainId: arcTestnetLaunchpadDeployment.chainId,
54
+ chainId: deployment.chainId,
46
55
  family: "evm",
47
- nativeCurrency: { symbol: "USDC", decimals: 18 },
56
+ nativeCurrency: { symbol: "ARC", decimals: 18 },
48
57
  rpcUrls: ["https://rpc.testnet.arc.io"],
49
58
  explorerUrl: "https://testnet.arcscan.app",
50
- apiBaseUrl: "https://your-indexer.example",
59
+ apiBaseUrl: "https://api.testnet.arc.example",
51
60
  realtimeUrl: "wss://rpc.testnet.arc.io",
52
- deployment: arcTestnetLaunchpadDeployment,
53
- quoteTokens: [
54
- { address: arcTestnetLaunchpadDeployment.contracts.quoteToken, decimals: 6, symbol: "tUSDC" },
55
- ],
56
- capabilities: {
57
- create: true,
58
- bondingTrade: true,
59
- graduatedTrade: false, // 尚无经审计的 Router
60
- antiFarmer: true,
61
- earlySellTax: false,
62
- rewards: false, // Locker claim 在产品确认前保持关闭
63
- referral: false,
64
- },
61
+ deployment,
62
+ // 一个 factory 支持多个 QuoteToken,把你要提供的币种都列出来。
63
+ quoteTokens: deployment.economics.quoteTokens.map((quoteToken) => ({
64
+ address: quoteToken.address,
65
+ decimals: quoteToken.decimals,
66
+ symbol: quoteToken.symbol,
67
+ })),
68
+ capabilities: { create: true, bondingTrade: true, partnerAttribution: true },
65
69
  confirmations: 2,
66
70
  });
67
71
 
68
- // 校验链 ID、Portal runtime bytecode hash 及全部模块绑定。
69
72
  await verifyLaunchpadDeployment(publicClient, chain.deployment);
70
-
71
- const client = createLaunchpadPortalClient({ publicClient, walletClient, chain });
73
+ const client = createLaunchpadFactoryClient({ publicClient, walletClient, chain });
72
74
  ```
73
75
 
74
- 正式网上线后按同样方式注册 manifest,调用方代码无需改动。
76
+ `verifyLaunchpadDeployment` 分三类校验,因为它们失败的原因完全不同:factory 的 runtime code hash
77
+ (身份)、factory 报告的每个模块地址加上 hook 自己看到的 V4 PoolManager(接线)、以及 fee policy、
78
+ partner policy、狙击税、launch fee、creator 上限和每个 pinned 的 QuoteToken 与 LaunchConfig(经济参数)。
79
+ 接入新部署前一定要跑:V2 的依赖是构造后用一组独立 setter 逐个接上的,接错一半的部署在调用真正打到零地址
80
+ 之前看起来一切正常。
75
81
 
76
- 写入路径受上述 capabilities 门控,不满足时抛 `UnsupportedLaunchpadCapabilityError`:
77
- `prepareCreateToken` 需要 `create`,`prepareBuy`/`prepareSell` 需要 `bondingTrade`,
78
- `prepareClaimReward` 需要 `rewards`(默认关闭)。`prepareMigration` 与 `prepareCollectFees` **不**受
79
- 门控 —— 两者在链上都是 permissionless;禁用迁移会让已毕业的 Token 卡住,禁用手续费收取则会冻住
80
- 本属于 creator 的收益。读取永不受门控,因此页面在无签名者时也能渲染。
81
-
82
- ## 读取
83
-
84
- 所有 read 都接受 `blockNumber` / `blockTag`,可以把状态、报价、余额和 allowance 固定在同一个区块,
85
- 避免拼接不同高度的数据。
82
+ ## 读取 Launch 状态
86
83
 
87
84
  ```ts
85
+ // 先 pin 住区块。curve 快照是由很多个 getter 拼出来的,不 pin 会混用不同高度,
86
+ // 拼出一个从未同时存在过的储备状态。
88
87
  const blockNumber = await publicClient.getBlockNumber();
89
- const [state, quote, availability] = await Promise.all([
90
- client.getTokenState(token, { blockNumber }),
91
- client.quoteBuy(token, parseUnits("100", 6), { blockNumber }),
92
- client.getTradeAvailability(token, { blockNumber }),
93
- ]);
94
- ```
95
88
 
96
- `getTradeAvailability` 与 `getPrice` 是组合读取,**严格同块**:先解析出一个确定高度,再把各子读取
97
- 固定到它,因此不会用某个区块的状态去计算另一个区块的费率。`latest`、`safe`、`finalized` 与显式高度
98
- 语义一致;`pending` 没有稳定高度,会以 `UnpinnableBlockTagError` 拒绝。
89
+ const state = await client.getTokenState(token, { blockNumber });
90
+ state.record; // 创建时冻结的经济参数(curve 地址、quote token、creator tax、各项分成)
91
+ state.curveState; // 实时储备、可售量、是否已到毕业边界
92
+ state.graduation; // "notGraduated" | "swept" | "poolCreated" | "emergencyExited"
99
93
 
100
- `getPrice` 返回的是**曲线参考价**,并标注 `source` 与读取时的 `phase`。它不是可执行报价,不能用来
101
- 计算 `minOut`;毕业之后它也不会跟随 V3 池价格,实时 DEX 价格请从官方 Pool 或 Indexer 获取。
94
+ const progress = await client.getCurveProgress(token, { blockNumber });
95
+ progress.progressBps; // 按 Token 侧衡量,所以恰好封顶在 100%
96
+ ```
102
97
 
103
- ## 交易
98
+ 用户粘贴进来的地址必须先过 `isLaunchToken`。和 V3 不同,V2 的 record getter 对陌生地址**不 revert**,
99
+ 而是返回一个全零结构体——跳过这个检查会让后续每一次调用都打到零地址。
104
100
 
105
- 写入统一遵循:参数校验 → 刷新报价 → 计算非零最小输出 → 检查精确授权 → `simulateContract` →
106
- `writeContract`。没有经过模拟的交易不会被广播。
101
+ ## 交易
107
102
 
108
103
  ```ts
109
- import { toDeadline } from "@masterpeach/launchpad-sdk";
104
+ const curve = await client.getCurveAddress(token);
110
105
 
111
- const block = await publicClient.getBlock();
112
- const prepared = await client.prepareBuy({
106
+ // curve 拉取 quote,所以 spender 是 curve。
107
+ await client.send(
108
+ await client.prepareApprove({ asset: quoteToken, spender: curve, amount: quoteIn }),
109
+ );
110
+
111
+ const buy = await client.prepareBuy({
113
112
  token,
114
- quoteIn,
115
- slippage: { bps: 100 }, // 上限 10%
116
- deadline: toDeadline(block.timestamp),
113
+ quoteInRaw: quoteIn,
114
+ slippage: { bps: 100 },
115
+ deadline,
116
+ tokenRecipient, // 决定狙击税;不传则用当前连接账户
117
+ partnerId, // 可选归因;bytes32(0) 或不传表示无 Partner
117
118
  });
119
+ const result = await client.wait(await client.send(buy));
120
+ ```
121
+
122
+ `buy.preview` 会给出 `tokenOut`、`quoteSpent`、`refund`、三项费用明细,以及实际适用的 `snipeTaxBps`。
123
+ 在一条 curve 的收尾买入里,`quoteSpent` 会小于你报的金额,差额在同一笔交易里原子退回——授权和展示都要用
124
+ `quoteSpent`,不要用用户输入的数字。
125
+
126
+ 卖出动的是 launch token,需要它自己的授权,spender 同样是 curve。卖出永远不收狙击税。
127
+
128
+ ### 收尾买入的滑点陷阱
118
129
 
119
- const hash = await client.send(prepared);
120
- const result = await client.wait(hash);
130
+ 合约按实际花费等比缩放最低成交量:`mulDiv(minTokenOut, quoteSpent, quoteInRaw)`,RC2 起向上取整
131
+ (RC1 是向下取整,所以现在的舍入对用户略有利)。但这仍然会让保护**按退款比例等比减弱**。报价约为实际所需两倍时,有效最低量直接砍半;再往上,任何用户物理上能填的 `minTokenOut`
132
+ (哪怕填满整个 supply)都无法生效。
133
+
134
+ 想在收尾那笔上要一个真实的下限,就先报价、再按实际需要的金额下单;**不要**报一个大整数指望
135
+ `minTokenOut` 兜底。
136
+
137
+ ## 创建 Token
138
+
139
+ ```ts
140
+ import { UNCHECKED_ECONOMICS, launchpadEconomicsHashFromDeployment } from "@masterpeach/launchpad-sdk";
141
+
142
+ const gate = await client.getLaunchGate(creator);
143
+ // gate.canLaunch,以及背后的三个输入:launchEnabled / whitelisted / denied。
144
+ // Denylist 优先级最高:被拉黑的 creator 即使同时在白名单里也不能创建。
145
+
146
+ const params = {
147
+ name: "Peach Demo",
148
+ symbol: "PEACH",
149
+ logo: "ipfs://...",
150
+ description: "...",
151
+ socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
152
+ quoteToken,
153
+ launchConfigId: 0n,
154
+ // 把这次创建锁定到一份确切的经济参数模板。可选:传 bytes32(0) 就跳过校验。
155
+ expectedEconomics: launchpadEconomicsHashFromDeployment(client, { quoteToken, launchConfigId: 0n }),
156
+ launchTaxExemptions: [],
157
+ creatorFeeRecipient: zeroAddress, // 链上会解析成 creator 本人;这里原样保留不代填
158
+ buybackEnabled: true,
159
+ creatorTaxBps: 100,
160
+ creatorInitialBuyQuoteRaw: 0n,
161
+ creatorInitialBuyRecipient: creator,
162
+ minInitialTokenOut: 0n,
163
+ deadline,
164
+ salt,
165
+ };
166
+
167
+ const predicted = await client.predictAddresses(params, creator); // { token, curve }
168
+ const created = await client.wait(await client.send(await client.prepareCreateToken(params)));
169
+ const launch = created.events.tokenCreated[0]; // 唯一能证明创建成功的依据
121
170
  ```
122
171
 
123
- 授权默认是**精确额度**,spender 恒为 Portal。无限授权必须显式传入 `maxUint256`。买入时应按
124
- `quote.quoteSpent` 授权,而不是用户输入值 —— 触达毕业的买入实际花费会更少。
172
+ `prepareCreateToken` 会校验所有「纯粹由参数决定」的规则,并**一次性返回全部违规项**,这样表单可以一遍
173
+ 标出所有错误字段。长度按 UTF-8 字节算,和合约一致:emoji 或中文的名字能通过朴素的 `.length <= 64`
174
+ 检查,但链上照样 revert。
175
+
176
+ 有两条规则容易漏。salt 每个 creator 只能用一次(用 `isLaunchpadSaltUsed` 查)。以及豁免地址会和
177
+ **自动豁免**的那几个一起去重——creator、fee recipient、首购 recipient 本来就已豁免,所以把 creator
178
+ 自己填进 `launchTaxExemptions` 会直接 revert,不是无害的冗余项。
179
+
180
+ `prepareLaunchAndBuy` 走 forwarder,用于在由另一个合约发起创建时保留 creator 身份。
181
+ **它的授权给 forwarder,不是 factory**——RC2 改成由 forwarder 精确收款、在调用期间临时授权 factory、
182
+ 调用结束后撤销授权并退回余额。这与 RC1 正好相反,从 RC1 迁过来的代码会授权错对象。
183
+
184
+ 创建也不保证 Launch 还留在内盘:如果首购吃掉了最后一份可售 Token,factory 会在同一笔交易里完成毕业。
185
+ 收到 `TokenCreated` 后请读 `getGraduation(token)`,不要默认它是 `notGraduated`。
125
186
 
126
- ### 创建 Token
187
+ ### Holder Fee Sharing(按持有人分成)
127
188
 
128
- 传入 `slippage`,SDK 会用已部署的 curve 为 creator 原子首购报价,并据此推导安全的非零
129
- `minInitialTokenOut`:
189
+ 可选模式:Launch 的 creator 费用不再进单一收款地址,而是进一个「每个 Launch 一份」的 distributor,
190
+ 由它与持有人分成。
130
191
 
131
192
  ```ts
132
- const prepared = await client.prepareCreateToken({
133
- name,
134
- symbol,
135
- metadata: JSON.stringify({ description, image, website }),
136
- userSalt: keccak256(crypto.getRandomValues(new Uint8Array(32))),
137
- initialQuoteIn, // 不首购则传 0n
138
- slippage: { bps: 100 }, // initialQuoteIn > 0 时必填
139
- deadline: toDeadline(block.timestamp),
140
- protectionBlocks: 6, // 0..6
193
+ const prepared = await client.prepareLaunchWithHolderFeeSharing({
194
+ ...params,
195
+ creatorFeeRecipient: zeroAddress, // 必须为零地址,见下
141
196
  });
197
+ prepared.preview.distributor; // 预测出来的地址,也是这笔交易将要安装的那个
198
+ ```
199
+
200
+ 一笔交易内完成:创建 distributor → 装成 `creatorFeeRecipient` → 创建 token 与 curve → 校验所有绑定。
201
+ 任何一步失败整笔回滚,不会留下半成品。
202
+
203
+ **`creatorFeeRecipient` 必须是零地址。** recipient 由 forwarder 自己推导。知道有 distributor 之后
204
+ 自己预测一个填进去——这个最自然的动作,恰恰是合约明确拒绝的。
205
+
206
+ distributor 由 `(creator, salt)` 决定,**不**由 token 决定:token 自己的地址要经 `creatorFeeRecipient`
207
+ 依赖 distributor,反过来预测会循环。签名前可以用 `predictLaunchpadHolderDistributor` 拿到地址。
208
+
209
+ **它不是不可逆的。** 链上没有任何东西把 distributor 钉死:当前 recipient 可以用
210
+ `transferCreatorFeeRecipient` 把后续收益转走,治理也能按常规延迟覆盖 recipient。留下来的只有注册表
211
+ 记录和 distributor 已经收到的资产。不要向用户承诺"开启 Holder Sharing 后就锁死了"。
212
+
213
+ **Buyback 最终也会分给持有人,但晚、而且是 token 形式。** 开了 buyback 之后,creator gross 的一部分
214
+ 会被用来买入 LaunchToken 存进 vault、按五年释放;`harvest` 会把已归属的那部分拉进 distributor,所以它
215
+ 确实会被分掉——只是作为后续某一期的 coin 侧,而不是交易当时的 quote。"全部 creator fee 立即分给持有人"
216
+ 和"开启 buyback"仍然是两个不同的承诺。
217
+
218
+ 领取也覆盖了。收益按 Epoch 发放:distributor 累积 creator 费用,publisher 用一个 Merkle Root 开一期,
219
+ 持有人在 90 天窗口内凭 proof 领取,没领完的滚回池子留给下一期。
142
220
 
143
- prepared.preview.firstBuy; // 推导最小值所依据的完整报价
144
- prepared.preview.minInitialTokenOut; // 实际提交的值
221
+ ```ts
222
+ const { distributor } = await getLaunchpadHolderBinding(ctx, token);
223
+ const epoch = await getLaunchpadHolderEpoch(ctx, { distributor, epochId });
224
+
225
+ // allocation 来自 distributor 项目的 Proof 服务
226
+ if (!launchpadHolderAllocationMatchesRoot(allocation, epoch.root)) throw new Error("stale proof");
227
+ await client.send(await prepareLaunchpadHolderClaim(ctx, { distributor, allocation }));
145
228
  ```
146
229
 
147
- allowance 按 `quoteSpent` 而非 `initialQuoteIn` 检查 —— 触达毕业阈值的首购会被截断,实际转账少于
148
- 承诺额;而 simulation 仍提交原始 `initialQuoteIn`,因为截断由 Portal 自己完成。若你自行计算最小值,
149
- 可用 `minInitialTokenOut` 替代 `slippage`,它必须为正且不超过实时报价,因此不存在零最小值的不安全
150
- 路径。
230
+ 每份 allocation **同时**发两边——quote 收益和 launch token,一次领完;而且任何人都能提交,因为付款按
231
+ `allocation.account` 走而不是按发起人。领之前先对着 epoch root 本地校验:合约的 `InvalidMerkleProof`
232
+ 说不出到底是金额、账户还是 epoch 错了,而最常见的原因是把 Proof 服务自己的数据库 epoch id 当成了链上
233
+ id——链上要的是 `onchain_epoch_id`。
234
+
235
+ 发布 Epoch 和提供 Proof 仍属于 distributor 项目,见[对接指南](docs/integration-guide.md#holder-claims)。
236
+
237
+ 交易回执里会**同时**出现两半:forwarder 的 `HolderSharingLaunchCreated` 与 distributor factory 的
238
+ `DistributorCreated`,都在 `events.holderSharing` 里。它们**故意不合并**——只带其中一个的 Launch
239
+ 不是官方的 Holder Sharing Launch。权威读法是 `getHolderDistributor(token)` 与
240
+ `isHolderDistributor(address)`。任何 Launch 都可以把 `creatorFeeRecipient` 填成任意地址,所以不查注册表
241
+ 就把它标成 holder distributor 是不对的。
151
242
 
152
- ### prepare → send 会二次校验
243
+ ## 毕业
153
244
 
154
- prepared transaction 携带它被模拟时的 chain、account 与 Portal,`send` 在广播前会重新读取钱包的
155
- 实时 chain 与 account:
245
+ ```ts
246
+ const graduation = await client.getGraduation(token);
156
247
 
157
- | 场景 | 错误 |
158
- | --- | --- |
159
- | prepare 后钱包切换了网络 | `WrongWalletChainError` |
160
- | prepare 后钱包切换了账户 | `PreparedAccountMismatchError` |
161
- | 属于其他 deployment,或非本 client 产出 | `ForgedPreparedTransactionError` |
248
+ // 第一步:把 curve 的资产 sweep 给 executor。curve 关闭后任何人都能调——而且常常已经被别人做掉了,
249
+ // 因为 RC2 会在清空 curve 的那笔买入里自动毕业。prepareGraduate 会直接拒绝,而不是构造一笔必然失败的交易。
250
+ if (graduation.status === "notGraduated" && state.curveState.graduationReady) {
251
+ await client.send(await client.prepareGraduate(token));
252
+ }
162
253
 
163
- prepare 与 send 请用同一个 client 实例;钱包发生任何变化后重新 prepare,不要重试过期的 prepared 值。
254
+ // 第二步:建 V4 池并铸出 position。同样无许可,且可重试。
255
+ if (graduation.status === "swept") {
256
+ await client.send(await client.prepareCreateGraduatedPool(token));
257
+ }
164
258
 
165
- ## 事件
259
+ const poolId = await client.getPoolId(token); // 池子存在之前是 bytes32(0)
260
+ ```
166
261
 
167
- 链上 `Trade.quoteAmount` 语义不对称:买入记录**扣费后**的 net,卖出记录**扣费前**的 gross。
168
- 解码器保留原始值,并额外给出方向明确的有符号增量,避免用一个含义随方向变化的字段统计成交量。
262
+ `swept` 是一个可以停留的状态,不是过渡态。第二步失败时资产安全地留在 executor,直接重试即可——这是相对
263
+ V3 那个全有或全无的 `migrate` 的实质改进。
264
+
265
+ 自动毕业尝试失败时 RC2 会发 `AutoGraduationFailed`,**但对应的买入或创建仍然成功**。
266
+ `parseLaunchpadReceiptEvents` 特意把它放进独立的 `autoGraduationFailures` 分组,就是为了避免前端把它
267
+ 显示成购买失败;Launch 只是停在 `notGraduated`,由常规的无许可路径接手。
268
+
269
+ ## 费用
270
+
271
+ 费用先在 curve 上累计,再经由单独的 sweep 步骤转出,最后落到 `FeeEscrow` 里由受益人自己领取。
169
272
 
170
273
  ```ts
171
- trades[0].traderQuoteDelta; // 买: -(raw + fee),卖: +(raw - fee)
172
- trades[0].reserveQuoteDelta; // 买: +raw, 卖: -raw
274
+ const pending = await client.getPendingFees(curve); // 尚未 sweep
275
+ const claimable = await client.getClaimableBalance({ recipient, asset }); // 已在 escrow 里
276
+ await client.send(await client.prepareClaim({ asset })); // 领取自己的余额
173
277
  ```
174
278
 
175
- 新建 Token 地址一律取自 `TokenCreated` 事件,不使用预测地址。
279
+ **Escrow 只按 `(recipient, asset)` 两维记账。** 它不记录某笔 credit 来自哪个 Launch,所以同一个
280
+ recipient 被多个 Launch 复用时,每种资产只会汇成一个无法拆分的池子,链上没有办法还原归属。需要按
281
+ Launch 归因的话,要么每个 Launch 用不同的 recipient 地址,要么从 `CurveFeesSwept` / `FeeCredited`
282
+ 事件在链下自行重建。
176
283
 
177
- ## 错误
284
+ ## Partner
178
285
 
179
- 合约失败按解码出的 custom error 名称分类,不依赖错误文本:
286
+ Partner 是**按笔交易声明**的,不在创建时绑定到 token,而且比例是实时从注册表读的——所以调整比例从下一笔
287
+ 交易开始生效,绝不追溯。
180
288
 
181
- | 合约 revert | SDK 错误 |
182
- | --- | --- |
183
- | `InvalidStatus` | `LaunchpadPhaseUnavailableError` |
184
- | `ProtocolPaused` | `LaunchpadTradingPausedError` |
185
- | `LaunchBlockTradingDisabled` | `LaunchpadProtectionError` |
186
- | `SlippageExceeded` | `LaunchpadSlippageError` |
187
- | `DeadlineExpired` | `LaunchpadDeadlineExpiredError` |
188
- | allowance 不足 | `LaunchpadApprovalRequiredError` |
189
- | 余额不足 | `LaunchpadInsufficientBalanceError` |
190
- | `OfficialPoolLocked` | `LaunchpadOfficialPoolLockedError` |
191
- | LP NFT 编号未注册 | `LaunchpadUnknownPositionError` |
192
- | 钱包链不匹配 | `WrongWalletChainError` |
193
- | prepare 后切换了账户 | `PreparedAccountMismatchError` |
194
- | prepared 值来源不明 | `ForgedPreparedTransactionError` |
195
- | 该链 capability 已关闭 | `UnsupportedLaunchpadCapabilityError` |
196
- | 组合读取传入不可固定的 block tag | `UnpinnableBlockTagError` |
289
+ ```ts
290
+ const usability = await client.getPartnerUsability(partnerId);
291
+ ```
292
+
293
+ 三种结果,前端不能把它们混为一谈:
197
294
 
198
- 未识别的失败回退到 SDK 的共享错误码,并保留 cause。
295
+ | | `usable` | `earning` | 对交易的影响 |
296
+ |---|---|---|---|
297
+ | `bytes32(0)` | 是 | 否 | 正常成交,无归因 |
298
+ | 已注册、已禁用 | 是 | 否 | 正常成交,事件里仍记录,但拿不到分成 |
299
+ | 未注册的非零 ID | **否** | 否 | **整笔交易 revert** |
199
300
 
200
- ### 同时使用多个 MasterPeach SDK
301
+ 在已毕业的 V4 池上做 swap 时,用 `encodeLaunchpadPartnerHookData` 编码。冻结的 ABI 就是
302
+ `abi.encode(bytes32)`,无 Partner 时是空 bytes;hook 会严格校验长度,其他任何形式都会 revert
303
+ 而不是被忽略。
201
304
 
202
- 共享基础能力统一从 `./core` 子路径导出,**不会**出现在根入口,因此两个 MasterPeach SDK 的根入口不存在
203
- 同名导出,同时引入不会冲突。需要共享能力时,从任一 SDK 的 `./core` 导入:
305
+ ## Buyback 与紧急退出
204
306
 
205
307
  ```ts
206
- import { isPeachSdkError, parseAmount } from "@masterpeach/launchpad-sdk/core";
308
+ const vest = await client.getBuybackVest(token);
309
+ await client.send(await client.prepareBuybackRelease(token));
207
310
  ```
208
311
 
209
- 每个 SDK 仍会内联各自的副本,因此共享错误类在各 Package 中是**互相独立的对象**。请使用
210
- `isPeachSdkError` 判别,而不是 `instanceof`:
312
+ Vesting 时钟是对「历史全部锁仓」的单一五年加权平均,不是按笔分 tranche:每次新存入都会把整个余额的
313
+ `vestingEnd` 往后推,所以只要 buyback 持续有流入,这个 Launch 就永远不会完全归属完。
314
+
315
+ `prepareSetBuybackEnabled` 的权限在链上是不对称的:当前 creator fee recipient 可以**开也可以关**,
316
+ 而 protocol owner **只能关**。
211
317
 
212
318
  ```ts
213
- import { isPeachSdkError } from "@masterpeach/launchpad-sdk/core";
319
+ import { launchpadEmergencyExitAvailableAt } from "@masterpeach/launchpad-sdk";
214
320
 
215
- if (isPeachSdkError(error, "UNSUPPORTED_CHAIN")) {
216
- // 无论由 launchpad-sdk 还是 lending-sdk 抛出都成立。
217
- }
321
+ const availableAt = launchpadEmergencyExitAvailableAt(graduation); // 非 swept 时返回 undefined
322
+ const { distributor } = await client.getEmergencyDistributor({ launchToken: token, quoteToken });
323
+ const quote = await client.quoteEmergencyRedeem({ distributor, tokenIn });
218
324
  ```
219
325
 
220
- 该 Guard 通过全局 Symbol 注册表为错误打标记,因此每份副本都能识别其他副本抛出的错误。分支判断应以
221
- `PeachSdkErrorCode` 取值为稳定契约。上表中的 Launchpad 专有错误只存在于本 Package 且从根入口导出,
222
- 仍应使用 `instanceof LaunchpadError` 判别。
326
+ 触发退出是 **owner-only** 的,与两步毕业不同——被套住的持有人只能在治理触发之后赎回,自己拉不动这个闸。
223
327
 
224
- ## 重新生成 ABI
328
+ 紧急退出的适用面比「卡住的 Launch」窄得多:它要求 Launch **已经处于 `swept`**,并从 `sweptAt` 起算
329
+ 七天,因为它填的正是两步毕业之间的那个缺口。还在 curve 上交易的 Launch 根本无从退出,而且没有任何快速通道。
225
330
 
226
- ABI 由固定合约 revision 的 Foundry artifact 生成,禁止手写或从应用侧复制。
331
+ 赎回的分母是代币的**实时** `totalSupply()`,每次重新读取而非快照。因此有人直接 burn 而不赎回时,会抬高
332
+ 其余所有人的单位赎回额,最后一个赎回的人拿走全部剩余。报价会随别人的行为漂移,提交前请重新读取。
227
333
 
228
- ```bash
229
- # 在合约仓库的固定 revision 上
230
- forge build
231
-
232
- pnpm abi:generate \
233
- --config tooling/abi-codegen/launchpad-artifacts.json \
234
- --artifacts-root <contracts>/contracts/out \
235
- --output-dir packages/launchpad/src/abi/generated \
236
- --artifact-commit <40 位 commit>
237
- ```
334
+ ## Keeper 操作
335
+
336
+ Curve sweep 和 V4 池的 Epoch 分页 Partner 结算在 keeper 接口里。注意一个很容易搞错的参数不对称:
337
+ `beginPoolFeeSweep` 收的是 **token**,而 `settlePoolFeeSweepPartners` 和 `finalizePoolFeeSweep`
338
+ 收的是 **poolId**(用 `getLaunchpadPoolId` 解析)。协议对 Partner 数量和分页大小都**没有上限**,
339
+ 每批多少完全取决于你自己的 gas 预算。
340
+
341
+ RC2 改变了 sweep 的失败方式:碰到内部价格影响上限不再整笔回滚,而是能换多少换多少,剩下的留在
342
+ Pending,Partner 未结算的部分滚入下一个 Epoch。`InternalPriceImpactExceeded` 已经不存在,所以没有
343
+ 异常可捕获重试——改为读 `getLaunchpadPartnerEpochAccounting({ poolId, epochId })`,在
344
+ `allPartnersSettled` 为 false 时继续分页,只在 `finalizeReady` 为 true 时才 finalize(链上校验的是
345
+ 五个条件,不只是 Partner 计数),并把 `partnerLaunchInputConsumed < partnerLaunchInputTotal`
346
+ 当作"还有活没干完"而不是错误。
347
+
348
+ 还有一个 RC2 之前就存在的坑:`MemeHook.settlingEpoch` 是**一基**存储的,0 表示没有正在结算的 Epoch。
349
+ Epoch id 请取 `getLaunchpadPoolSweepState(...).settlingEpochId`(已解码),直接用原始值会大 1,
350
+ 导致 settle 和 finalize 全部 revert。
351
+
352
+ 另外 curve 的 `sweepFees` 可能顺带触发毕业:它内部的 buyback 有可能买走最后的可售 Token。每次 sweep
353
+ 成功后都要重新读一次毕业状态。
354
+
355
+ ## 错误处理
356
+
357
+ 这套部署能产生的每一个合约 revert 都映射到稳定、可捕获的类型,且映射表对生成的 ABI 做了穷尽性断言。
358
+ 值得单独分支处理的几个:
359
+
360
+ - `LaunchpadTradingClosedError` —— curve 已到边界。**永久性**关闭,之后去 V4 池交易。
361
+ - `LaunchpadTradeFeeConsumesInputError` —— 费用吃光了输入。加大金额,或等狙击税衰减。
362
+ - `LaunchpadInsufficientQuoteReserveError` —— 卖出超过了 curve 真实(非 phantom)的储备。
363
+ - `LaunchpadSlippageError`、`LaunchpadDeadlineExpiredError` —— 重新报价并重建交易。
364
+ - `LaunchpadCreatorDeniedError` 与 `LaunchpadLaunchNotPermittedError` —— 前者是针对单个账户的封禁,
365
+ 后者是对所有非白名单账户关闭的闸门,处理方式完全不同。
366
+ - `LaunchpadEconomicsHashMismatchError` —— 治理改了模板;重新读取并重建哈希。
367
+ - `LaunchpadApprovalRequiredError` —— 带着确切的 `spender`,而它因操作而异。
368
+ - `prepareGraduate` 抛出的 `LaunchpadPhaseUnavailableError` —— 在 RC2 下这通常意味着别人的交易已经把
369
+ 它毕业了。看 `error.status`:`launchpadGraduateIsIdempotentSuccess(status)` 对 `swept` 和
370
+ `poolCreated` 返回 true,keeper 应当按"已完成"处理而不是告警。
371
+
372
+ ## 主网
373
+
374
+ 合约与经济参数完全相同,只剩一处不同。主网已于 2026-09-08 轮换到 Holder-aware forwarder 并绑定了
375
+ distributor factory,两条创建路径和 Holder Fee Sharing 的行为与测试网完全一致——只有地址不同。仍然不
376
+ 同的是 Quote Token:只有 USDC,tUSDG 是测试网水龙头代币,照搬过去等于没有可交易资产。
377
+
378
+ Public Launch 已开启,创建、买入、卖出都已在链上跑通;毕业路径尚未验证。细节见
379
+ [对接指南](docs/integration-guide.md#mainnet-what-differs)。
380
+
381
+ ## 包入口
238
382
 
239
- ## 范围
383
+ - `@masterpeach/launchpad-sdk` —— 客户端、actions、math、类型与错误
384
+ - `@masterpeach/launchpad-sdk/abi` —— 生成的 Production V2 ABI
385
+ - `@masterpeach/launchpad-sdk/core` —— 跨 SDK 共享的链无关原语
240
386
 
241
- 不包含:Tax Token、Referral、跨链桥、聚合器 DEX Swap、协议 Owner 管理入口、V3 LP 管理。
242
- 毕业后的交易在经过审计的 Router 合约发布前保持 quote-only。
387
+ 完整生命周期、索引注意事项、keeper 运维手册和上线检查清单见
388
+ [对接指南](docs/integration-guide.md)。