@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.md CHANGED
@@ -1,14 +1,13 @@
1
- # MasterPeach Launchpad SDK
1
+ # @masterpeach/launchpad-sdk
2
2
 
3
- [简体中文](./README.zh-CN.md)
3
+ Typed, Viem-only SDK for the ARC Launchpad **Production V2** contracts. A launch sells its token on
4
+ a Pons-style bonding curve until the curve reaches a fixed token-side boundary, then graduates into
5
+ a Uniswap V4 pool in two permissionless, separately retryable steps.
4
6
 
5
- Typed, Viem-only TypeScript SDK for the ARC Launchpad. It covers deployment resolution and
6
- verification, generated ABI bindings, on-chain reads and quotes, exact-amount approvals,
7
- simulate-then-send write pipelines, and receipt event decoding.
8
-
9
- The SDK's only consumer-provided dependency is `viem`. Shared amount, chain, transaction, and error
10
- primitives are bundled and re-exported from the root entry point. It ships no React hooks, no Wagmi
11
- adapter, and no Indexer client — those belong in the application layer.
7
+ This package targets Production V2 only. It is **not** an upgrade of the previous V3 SDK: the
8
+ contract family was rewritten, so no address, ABI, or type from V3 carries over. The referrer
9
+ system, the standalone creator-denylist registry, the pause switch, one-step migration, and the
10
+ WAD-scaled curve are all gone.
12
11
 
13
12
  ## Install
14
13
 
@@ -16,247 +15,415 @@ adapter, and no Indexer client — those belong in the application layer.
16
15
  pnpm add @masterpeach/launchpad-sdk viem
17
16
  ```
18
17
 
19
- ## Entry points
18
+ ## The four things most likely to trip you up
19
+
20
+ Read these before writing any integration; each one produces a call that looks right and fails.
20
21
 
21
- | Import | Contents |
22
- | --- | --- |
23
- | `@masterpeach/launchpad-sdk` | Client, actions, domain types, deployments, errors, pure helpers |
24
- | `@masterpeach/launchpad-sdk/abi` | Generated ABI bindings and the artifact commit |
22
+ **1. Every launch has its own `BondingCurve`. Trades go to the curve, not the factory.** The factory
23
+ only creates launches and answers questions about them. Resolve the curve with `getCurveAddress`
24
+ (or read it from `TokenCreated` / `getLaunchRecord`) and cache it per token.
25
25
 
26
- Frontend integration is covered in depth in
27
- [docs/integration-guide.en.md](./docs/integration-guide.en.md)
28
- ([简体中文](./docs/integration-guide.md)), including a Wagmi adapter, query keys, and a migration
29
- checklist.
26
+ **2. Approval spenders differ by operation, and the forwarder address moved.** A trade's funds are
27
+ pulled by the **curve**. A direct `createToken` is pulled by the **factory**. Either forwarder entry
28
+ point is pulled by the **forwarder** itself. Read the forwarder from the deployment record rather
29
+ than pinning a literal — it was rotated on 2026-09-07 to the Holder-aware forwarder, and allowances
30
+ granted to the old one do not carry over. `prepareApprove` requires an explicit `spender`;
31
+ `launchpadCreationSpender(deployment, entryPoint)` returns the right one.
30
32
 
31
- ## Deployments and capabilities
33
+ **3. The opening snipe tax is resolved against the trade's token recipient, not its payer.** A
34
+ router buying on someone's behalf neither lends nor borrows its own exemption. Pass the real
35
+ recipient to `prepareBuy`, or you will quote the wrong rate and set an unreachable `minTokenOut`.
32
36
 
33
- Arc is **testnet-only** today; no mainnet exists. The built-in production registry is therefore
34
- empty by design and reserved for a future verified manifest. The Arc Testnet V2 deployment is
35
- exported by name so it must be opted into explicitly.
37
+ **4. A launch can graduate inside someone else's transaction.** RC2 graduates automatically when a
38
+ buy — including a creation's initial buy — takes the last sellable token. Never assume a freshly
39
+ created launch is `notGraduated`, and never call `graduate` without re-reading the status first.
40
+ The SDK sets an explicit gas limit on those transactions; if you rebuild one from `request` by hand,
41
+ keep the `gas` field, because a bare estimate leaves the in-transaction graduation short and it
42
+ always reverts out-of-gas.
36
43
 
37
- The typed client is configured from a **chain config**, which carries the verified deployment *and*
38
- the product capabilities. One object means there is no second source to drift, and the write gate
39
- cannot be skipped by construction.
44
+ ## Create a client
40
45
 
41
46
  ```ts
42
47
  import {
43
48
  arcTestnetLaunchpadDeployment,
44
- createLaunchpadPortalClient,
49
+ createLaunchpadFactoryClient,
45
50
  defineLaunchpadChain,
46
51
  verifyLaunchpadDeployment,
47
52
  } from "@masterpeach/launchpad-sdk";
48
53
 
54
+ // `arcMainnetLaunchpadDeployment` (chain 5042) is the other one. They share no address and no
55
+ // contractVersion; neither is a registered default, so pick one by name.
56
+ const deployment = arcTestnetLaunchpadDeployment;
57
+
49
58
  const chain = defineLaunchpadChain({
50
59
  slug: "arc-testnet",
51
- chainId: arcTestnetLaunchpadDeployment.chainId,
60
+ chainId: deployment.chainId,
52
61
  family: "evm",
53
- nativeCurrency: { symbol: "USDC", decimals: 18 },
62
+ nativeCurrency: { symbol: "ARC", decimals: 18 },
54
63
  rpcUrls: ["https://rpc.testnet.arc.io"],
55
64
  explorerUrl: "https://testnet.arcscan.app",
56
- apiBaseUrl: "https://your-indexer.example",
65
+ apiBaseUrl: "https://api.testnet.arc.example",
57
66
  realtimeUrl: "wss://rpc.testnet.arc.io",
58
- deployment: arcTestnetLaunchpadDeployment,
59
- quoteTokens: [
60
- { address: arcTestnetLaunchpadDeployment.contracts.quoteToken, decimals: 6, symbol: "tUSDC" },
61
- ],
62
- capabilities: {
63
- create: true,
64
- bondingTrade: true,
65
- graduatedTrade: false, // no audited router yet
66
- antiFarmer: true,
67
- earlySellTax: false,
68
- rewards: false, // Locker claim off until the product confirms it
69
- referral: false,
70
- },
67
+ deployment,
68
+ // One factory serves several quote tokens, so list every currency you offer.
69
+ quoteTokens: deployment.economics.quoteTokens.map((quoteToken) => ({
70
+ address: quoteToken.address,
71
+ decimals: quoteToken.decimals,
72
+ symbol: quoteToken.symbol,
73
+ })),
74
+ capabilities: { create: true, bondingTrade: true, partnerAttribution: true },
71
75
  confirmations: 2,
72
76
  });
73
77
 
74
- // Confirms chain id, Portal runtime bytecode hash, and every module binding before use.
75
78
  await verifyLaunchpadDeployment(publicClient, chain.deployment);
76
-
77
- const client = createLaunchpadPortalClient({ publicClient, walletClient, chain });
79
+ const client = createLaunchpadFactoryClient({ publicClient, walletClient, chain });
78
80
  ```
79
81
 
80
- When Arc mainnet ships, register its manifest the same way — call sites do not change.
82
+ `verifyLaunchpadDeployment` checks three separable things, because they fail for different reasons:
83
+ the factory's runtime code hash (identity), every module address the factory reports plus the hook's
84
+ own view of the V4 PoolManager (wiring), and the fee policy, partner policy, snipe tax, launch fee,
85
+ creator ceilings and every pinned quote token and launch config (economics). Run it before trusting
86
+ a newly configured deployment — V2 wires its dependencies with individual post-construction setters,
87
+ so a half-configured deployment looks fine until a call reaches address zero.
81
88
 
82
- Write paths are gated by these capabilities and throw `UnsupportedLaunchpadCapabilityError`:
83
- `prepareCreateToken` needs `create`, `prepareBuy`/`prepareSell` need `bondingTrade`, and
84
- `prepareClaimReward` needs `rewards` (off by default). `prepareMigration` and `prepareCollectFees`
85
- are **not** gated — both are permissionless on-chain, and disabling migration would strand
86
- graduated tokens while disabling collection would freeze fees that belong to creators. Reads are
87
- never gated, so a page renders without a signer regardless of which writes are enabled.
88
-
89
- ## Reading
90
-
91
- Every read accepts `blockNumber` / `blockTag`, so state, quotes, balances, and allowances can be
92
- pinned to one block instead of stitched from different heights.
89
+ ## Read a launch
93
90
 
94
91
  ```ts
92
+ // Pin a block. A curve snapshot is assembled from many getters, so an unpinned read can mix
93
+ // heights and produce reserves that never simultaneously existed.
95
94
  const blockNumber = await publicClient.getBlockNumber();
96
- const [state, quote, availability] = await Promise.all([
97
- client.getTokenState(token, { blockNumber }),
98
- client.quoteBuy(token, parseUnits("100", 6), { blockNumber }),
99
- client.getTradeAvailability(token, { blockNumber }),
100
- ]);
101
- ```
102
95
 
103
- `getTradeAvailability` and `getPrice` are composite reads and are **strictly same-block**: they
104
- resolve one height first, then pin their sub-reads to it, so a fee can never be computed for one
105
- block from another block's state. `latest`, `safe`, `finalized`, and an explicit height behave
106
- identically; `pending` has no stable height and is rejected with `UnpinnableBlockTagError`.
96
+ const state = await client.getTokenState(token, { blockNumber });
97
+ state.record; // frozen per-launch economics (curve address, quote token, tax, fee shares)
98
+ state.curveState; // live reserves, sellable supply, graduation readiness
99
+ state.graduation; // "notGraduated" | "swept" | "poolCreated" | "emergencyExited"
107
100
 
108
- `getPrice` returns the **bonding-curve reference price**, tagged with its `source` and the `phase`
109
- observed at read time. It is never an executable quote, it must not be used to derive a `minOut`,
110
- and after graduation it does not track the V3 pool — read the official pool or your indexer for a
111
- live DEX price.
101
+ const progress = await client.getCurveProgress(token, { blockNumber });
102
+ progress.progressBps; // measured on the token side, so it saturates at exactly 100%
103
+ ```
112
104
 
113
- ## Trading
105
+ An address a user pasted must go through `isLaunchToken` first. Unlike V3, the record getter does
106
+ not revert for a stranger address — it returns a zero-filled struct — so skipping the check leaves
107
+ every later call pointed at address zero.
114
108
 
115
- Writes follow one pipeline: validate → refresh quote → derive a non-zero minimum → check the exact
116
- allowance → `simulateContract` → `writeContract`. Nothing is broadcast without a simulation.
109
+ ## Trade
117
110
 
118
111
  ```ts
119
- import { toDeadline } from "@masterpeach/launchpad-sdk";
112
+ const curve = await client.getCurveAddress(token);
113
+
114
+ // The curve pulls the quote, so the curve is the spender.
115
+ await client.send(
116
+ await client.prepareApprove({ asset: quoteToken, spender: curve, amount: quoteIn }),
117
+ );
120
118
 
121
- const block = await publicClient.getBlock();
122
- const prepared = await client.prepareBuy({
119
+ const buy = await client.prepareBuy({
123
120
  token,
124
- quoteIn,
125
- slippage: { bps: 100 }, // capped at 10%
126
- deadline: toDeadline(block.timestamp),
121
+ quoteInRaw: quoteIn,
122
+ slippage: { bps: 100 },
123
+ deadline,
124
+ tokenRecipient, // determines the snipe tax; defaults to the connected account
125
+ partnerId, // optional attribution; bytes32(0) or omitted means none
127
126
  });
127
+ const result = await client.wait(await client.send(buy));
128
+ ```
129
+
130
+ `buy.preview` reports `tokenOut`, `quoteSpent`, `refund`, the three fee components and the
131
+ `snipeTaxBps` actually applied. On the final buy of a curve, `quoteSpent` is below the amount you
132
+ offered and the difference is refunded atomically — approve and display `quoteSpent`, never the
133
+ user's input.
134
+
135
+ Selling moves the launch token, so it needs its own approval, also to the curve. A sell never pays
136
+ snipe tax.
137
+
138
+ ### The slippage caveat on the last buy
139
+
140
+ The contract scales the minimum output by how much was actually spent:
141
+ `mulDiv(minTokenOut, quoteSpent, quoteInRaw)`, rounded up since RC2 (RC1 rounded down, so the
142
+ rounding now favours the trader). It still weakens the guard in proportion to the refund. Offering roughly twice what the final fill needs halves the effective minimum, and past
143
+ about that point no `minTokenOut` a trader could physically supply — not even the entire token
144
+ supply — can bind at all.
128
145
 
129
- const hash = await client.send(prepared);
130
- const result = await client.wait(hash);
131
- const trade = result.events.trades[0];
146
+ If you want a real floor on the closing buy, quote the fill first and offer close to it. Do not
147
+ offer a large round number and rely on `minTokenOut`.
148
+
149
+ ## Create a token
150
+
151
+ ```ts
152
+ import { UNCHECKED_ECONOMICS, launchpadEconomicsHashFromDeployment } from "@masterpeach/launchpad-sdk";
153
+
154
+ const gate = await client.getLaunchGate(creator);
155
+ // gate.canLaunch, and the three inputs behind it: launchEnabled / whitelisted / denied.
156
+ // The denylist outranks everything: a denied creator cannot launch even if also whitelisted.
157
+
158
+ const params = {
159
+ name: "Peach Demo",
160
+ symbol: "PEACH",
161
+ logo: "ipfs://...",
162
+ description: "...",
163
+ socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
164
+ quoteToken,
165
+ launchConfigId: 0n,
166
+ // Commits the launch to one exact economics template. Optional: bytes32(0) skips the check.
167
+ expectedEconomics: launchpadEconomicsHashFromDeployment(client, { quoteToken, launchConfigId: 0n }),
168
+ launchTaxExemptions: [],
169
+ creatorFeeRecipient: zeroAddress, // resolved to the creator on chain; preserved as given
170
+ buybackEnabled: true,
171
+ creatorTaxBps: 100,
172
+ creatorInitialBuyQuoteRaw: 0n,
173
+ creatorInitialBuyRecipient: creator,
174
+ minInitialTokenOut: 0n,
175
+ deadline,
176
+ salt,
177
+ };
178
+
179
+ const predicted = await client.predictAddresses(params, creator); // { token, curve }
180
+ const created = await client.wait(await client.send(await client.prepareCreateToken(params)));
181
+ const launch = created.events.tokenCreated[0]; // the only authority that a launch happened
132
182
  ```
133
183
 
134
- Approvals are **exact-amount** by default and the spender is always the Portal. An unlimited
135
- allowance requires passing `maxUint256` explicitly. For a buy, approve `quote.quoteSpent` rather
136
- than the user's typed input — a graduating buy spends less than requested.
184
+ `prepareCreateToken` validates every rule that is a pure function of the parameters and reports
185
+ **all** violations at once, so a form can mark every bad field in one pass. Byte lengths are UTF-8,
186
+ matching the contract: a name of emoji or CJK text can pass a naive `.length <= 64` check and still
187
+ revert.
188
+
189
+ Two rules are easy to miss. A salt is single-use per creator (`isLaunchpadSaltUsed` to check). And
190
+ launch tax exemptions are de-duplicated against the **automatic** ones — the creator, the fee
191
+ recipient, and the initial buy recipient are already exempt, so listing the creator is a revert,
192
+ not a redundant entry.
137
193
 
138
- ### Creating a token
194
+ `prepareLaunchAndBuy` routes through the forwarder, which preserves creator identity when another
195
+ contract initiates the launch. **Its allowance goes to the forwarder, not the factory** —
196
+ RC2 made the forwarder pull the exact amounts, hold a temporary factory approval for the duration of
197
+ the call, then revoke it and refund the residue. This is the reverse of RC1, so an integration
198
+ carried over from it approves the wrong contract.
139
199
 
140
- Pass `slippage` and the SDK prices the creator's atomic first buy against the deployed curve, then
141
- derives a safe non-zero `minInitialTokenOut` from it:
200
+ A creation is also not guaranteed to leave the launch on the curve. If the initial buy takes the
201
+ last sellable token, the factory graduates it in the same transaction — read
202
+ `getGraduation(token)` after `TokenCreated` rather than assuming `notGraduated`.
203
+
204
+ ### Holder Fee Sharing
205
+
206
+ An optional mode in which the launch's creator fees go to a per-launch distributor that shares them
207
+ with holders, instead of to a single recipient.
142
208
 
143
209
  ```ts
144
- const prepared = await client.prepareCreateToken({
145
- name,
146
- symbol,
147
- metadata: JSON.stringify({ description, image, website }),
148
- userSalt: keccak256(crypto.getRandomValues(new Uint8Array(32))),
149
- initialQuoteIn, // 0n for no first buy
150
- slippage: { bps: 100 }, // required when initialQuoteIn > 0
151
- deadline: toDeadline(block.timestamp),
152
- protectionBlocks: 6, // 0..6
210
+ const prepared = await client.prepareLaunchWithHolderFeeSharing({
211
+ ...params,
212
+ creatorFeeRecipient: zeroAddress, // required — see below
153
213
  });
154
-
155
- prepared.preview.firstBuy; // the full quote behind the minimum
156
- prepared.preview.minInitialTokenOut; // what will actually be submitted
214
+ prepared.preview.distributor; // predicted, and installed by the transaction itself
157
215
  ```
158
216
 
159
- The allowance is checked against `quoteSpent` rather than `initialQuoteIn`, because a first buy that
160
- reaches graduation is truncated at the threshold and transfers less than committed — while the
161
- simulation still submits the raw `initialQuoteIn`, since the Portal performs that truncation itself.
162
- `minInitialTokenOut` may be supplied instead of `slippage` when you compute it yourself; it must be
163
- positive and within the fresh quote, so there is no unsafe zero-minimum path.
217
+ One transaction creates the distributor, installs it as `creatorFeeRecipient`, creates the token and
218
+ curve, and verifies every binding; any failure reverts the lot.
164
219
 
165
- ### prepare → send is re-validated
220
+ **`creatorFeeRecipient` must be the zero address.** The forwarder derives the recipient itself.
221
+ Predicting the distributor and passing it in — the natural move once you know one is involved — is
222
+ exactly what the contract rejects.
166
223
 
167
- A prepared transaction carries the chain, account, and Portal it was simulated under, and `send`
168
- re-reads the wallet's live chain and account before broadcasting:
224
+ The distributor is keyed by `(creator, salt)`, never by the token: the token's own address depends
225
+ on the distributor through `creatorFeeRecipient`, so predicting it the other way round would be
226
+ circular. `predictLaunchpadHolderDistributor` gives you the address before signing.
227
+
228
+ **It is not irreversible.** Nothing on chain pins the distributor in place: the current recipient
229
+ can `transferCreatorFeeRecipient` fees away from it, and governance can override the recipient on
230
+ the usual delay. What survives is the registry entry and whatever the distributor already holds. Do
231
+ not tell users that enabling holder sharing locks it in.
232
+
233
+ **Buyback reaches holders too, but late and as tokens.** With buyback enabled, part of the creator's
234
+ gross buys the launch token into the vault and vests over five years. `harvest` pulls whatever has
235
+ vested into the distributor, so it does end up shared — as the coin side of a later epoch, not as
236
+ quote at trade time. "All creator fees shared immediately" and "buyback enabled" are still different
237
+ promises.
238
+
239
+ Claiming is covered too. Revenue reaches holders in epochs: the distributor accumulates creator
240
+ fees, its publisher opens an epoch with a merkle root, holders claim against it for 90 days, and the
241
+ remainder rolls over.
242
+
243
+ ```ts
244
+ const { distributor } = await getLaunchpadHolderBinding(ctx, token);
245
+ const epoch = await getLaunchpadHolderEpoch(ctx, { distributor, epochId });
246
+
247
+ // `allocation` comes from the distributor project's proof service.
248
+ if (!launchpadHolderAllocationMatchesRoot(allocation, epoch.root)) throw new Error("stale proof");
249
+ await client.send(await prepareLaunchpadHolderClaim(ctx, { distributor, allocation }));
250
+ ```
169
251
 
170
- | Situation | Error |
171
- | --- | --- |
172
- | Wallet switched network after prepare | `WrongWalletChainError` |
173
- | Wallet switched account after prepare | `PreparedAccountMismatchError` |
174
- | Prepared for another deployment, or not minted by this client | `ForgedPreparedTransactionError` |
252
+ Each allocation pays **both** sides — quote revenue and launch tokens — in one claim, and anyone may
253
+ submit it, since the payout follows `allocation.account` rather than the sender. Validate against
254
+ the epoch root first: the contract's `InvalidMerkleProof` cannot say whether the amount, the account
255
+ or the epoch was wrong, and the commonest cause is using the proof service's own database epoch id
256
+ where the contract wants `onchain_epoch_id`.
175
257
 
176
- Prepare and send through the same client instance, and re-prepare after any wallet change rather
177
- than retrying a stale prepared value.
258
+ Publishing epochs and serving proofs stay with the distributor project — see the
259
+ [integration guide](docs/integration-guide.md#holder-claims).
178
260
 
179
- ## Events
261
+ Afterwards, the receipt carries **both** halves of the creation — `HolderSharingLaunchCreated` from
262
+ the forwarder and `DistributorCreated` from the distributor factory — in `events.holderSharing`.
263
+ They are deliberately not merged: a launch carrying only one of the two is not an official
264
+ holder-sharing launch. `getHolderDistributor(token)` and `isHolderDistributor(address)` are the
265
+ authoritative reads. Any launch may nominate any `creatorFeeRecipient` it likes, so do not present
266
+ an arbitrary one as a holder distributor without checking the registry.
180
267
 
181
- `Trade.quoteAmount` is asymmetric on-chain: the Portal emits the **net** quote on a buy and the
182
- **gross** quote on a sell. The decoder preserves the raw value and adds unambiguous signed deltas,
183
- so volume is never computed from a field that changes meaning by direction.
268
+ ## Graduation
184
269
 
185
270
  ```ts
186
- const { trades, tokenCreated, migrationStarted } = result.events;
187
- trades[0].traderQuoteDelta; // buy: -(raw + fee), sell: +(raw - fee)
188
- trades[0].reserveQuoteDelta; // buy: +raw, sell: -raw
271
+ const graduation = await client.getGraduation(token);
272
+
273
+ // Step 1: sweep the curve's assets to the executor. Permissionless once the curve closes — and
274
+ // frequently already done for you, since RC2 graduates automatically at the end of the buy that
275
+ // empties the curve. prepareGraduate refuses rather than building a doomed transaction.
276
+ if (graduation.status === "notGraduated" && state.curveState.graduationReady) {
277
+ await client.send(await client.prepareGraduate(token));
278
+ }
279
+
280
+ // Step 2: create the V4 pool and mint the position. Permissionless and retryable.
281
+ if (graduation.status === "swept") {
282
+ await client.send(await client.prepareCreateGraduatedPool(token));
283
+ }
284
+
285
+ const poolId = await client.getPoolId(token); // bytes32(0) until the pool exists
189
286
  ```
190
287
 
191
- The created token address comes from the `TokenCreated` event, never from a predicted address.
288
+ `swept` is a resting state, not a transient one. If step two fails, the assets stay safely with the
289
+ executor and the call can simply be retried — this is the substantive improvement over V3's
290
+ all-or-nothing `migrate`.
192
291
 
193
- ## Errors
292
+ When an automatic graduation attempt reverts, RC2 emits `AutoGraduationFailed` and the **trade or
293
+ creation still succeeds**. `parseLaunchpadReceiptEvents` puts these in their own
294
+ `autoGraduationFailures` bucket precisely so a UI cannot mistake one for a failed purchase; the
295
+ launch simply stays `notGraduated` and the ordinary permissionless path picks it up.
296
+
297
+ ## Fees
194
298
 
195
- Contract failures are classified by decoded custom-error name, not message text:
196
-
197
- | Contract revert | SDK error |
198
- | --- | --- |
199
- | `InvalidStatus` | `LaunchpadPhaseUnavailableError` |
200
- | `ProtocolPaused` | `LaunchpadTradingPausedError` |
201
- | `LaunchBlockTradingDisabled` | `LaunchpadProtectionError` |
202
- | `SlippageExceeded` | `LaunchpadSlippageError` |
203
- | `DeadlineExpired` | `LaunchpadDeadlineExpiredError` |
204
- | insufficient allowance | `LaunchpadApprovalRequiredError` |
205
- | insufficient balance | `LaunchpadInsufficientBalanceError` |
206
- | `OfficialPoolLocked` | `LaunchpadOfficialPoolLockedError` |
207
- | unregistered LP NFT id | `LaunchpadUnknownPositionError` |
208
- | wallet on the wrong chain | `WrongWalletChainError` |
209
- | wallet switched account after prepare | `PreparedAccountMismatchError` |
210
- | prepared value from elsewhere | `ForgedPreparedTransactionError` |
211
- | capability disabled for this chain | `UnsupportedLaunchpadCapabilityError` |
212
- | non-pinnable block tag on a composite read | `UnpinnableBlockTagError` |
213
-
214
- Unrecognized failures fall back to the SDK's shared error codes with the cause preserved.
215
-
216
- ### Using more than one MasterPeach SDK
217
-
218
- Shared primitives are exported from the `./core` subpath, never from the root entry point, so the
219
- root entry points of two MasterPeach SDKs never export the same name. Import them from one SDK's
220
- `./core`:
299
+ Fees accrue on the curve, are swept out in a separate step, and land in `FeeEscrow` where the
300
+ beneficiary claims them.
221
301
 
222
302
  ```ts
223
- import { isPeachSdkError, parseAmount } from "@masterpeach/launchpad-sdk/core";
303
+ const pending = await client.getPendingFees(curve); // not yet swept
304
+ const claimable = await client.getClaimableBalance({ recipient, asset }); // in the escrow
305
+ await client.send(await client.prepareClaim({ asset })); // claims your own balance
224
306
  ```
225
307
 
226
- Each SDK still bundles its own copy, so the shared error classes are distinct objects in each
227
- package. Use the `isPeachSdkError` guard, not `instanceof`:
308
+ **The escrow keys balances by `(recipient, asset)` and nothing else.** It does not record which
309
+ launch a credit came from, so a recipient reused across several launches accumulates one
310
+ indistinguishable pool per asset that the chain cannot attribute back. If you need per-launch
311
+ attribution, use a distinct recipient address per launch, or reconstruct it off-chain from
312
+ `CurveFeesSwept` / `FeeCredited` events.
313
+
314
+ ## Partners
315
+
316
+ A partner is declared per trade rather than bound to a token at creation, and the registry is read
317
+ live, so a share change applies from the next trade onward and never retroactively.
228
318
 
229
319
  ```ts
230
- import { isPeachSdkError } from "@masterpeach/launchpad-sdk/core";
320
+ const usability = await client.getPartnerUsability(partnerId);
321
+ ```
231
322
 
232
- if (isPeachSdkError(error, "UNSUPPORTED_CHAIN")) {
233
- // Matches whether launchpad-sdk or lending-sdk raised it.
234
- }
323
+ Three outcomes that a UI must not collapse:
324
+
325
+ | | `usable` | `earning` | effect on the trade |
326
+ |---|---|---|---|
327
+ | `bytes32(0)` | yes | no | trades normally, no attribution |
328
+ | registered, disabled | yes | no | trades normally, recorded in the event, earns nothing |
329
+ | unregistered, non-zero | **no** | no | **reverts the whole trade** |
330
+
331
+ For a swap on a graduated V4 pool, encode the id with `encodeLaunchpadPartnerHookData`. The frozen
332
+ ABI is exactly `abi.encode(bytes32)`, and empty bytes for no partner; the hook validates the length
333
+ strictly, so anything else reverts rather than being ignored.
334
+
335
+ ## Buyback and emergency exit
336
+
337
+ ```ts
338
+ const vest = await client.getBuybackVest(token);
339
+ await client.send(await client.prepareBuybackRelease(token));
235
340
  ```
236
341
 
237
- The guard brands errors through the global symbol registry, so every copy recognizes the others'.
238
- `PeachSdkErrorCode` values are the stable contract to branch on. Launchpad-specific errors above
239
- exist only in this package and are exported from the root entry point, so `instanceof
240
- LaunchpadError` remains correct for them.
342
+ The vesting clock is a single five-year weighted average over everything ever locked, not per-deposit
343
+ tranches: each new deposit pushes `vestingEnd` further out for the whole balance, so a launch with
344
+ steady buyback flow never fully vests while the flow continues.
241
345
 
242
- ## Regenerating the ABI
346
+ `prepareSetBuybackEnabled` has an asymmetric permission enforced on chain: the current creator fee
347
+ recipient may turn buyback on **or** off, while the protocol owner may only turn it **off**.
243
348
 
244
- ABI is generated from Foundry artifacts built at a pinned contract revision; it is never
245
- hand-written or copied from an application.
349
+ ```ts
350
+ import { launchpadEmergencyExitAvailableAt } from "@masterpeach/launchpad-sdk";
246
351
 
247
- ```bash
248
- # in the contracts repo, at the pinned revision
249
- forge build
250
-
251
- pnpm abi:generate \
252
- --config tooling/abi-codegen/launchpad-artifacts.json \
253
- --artifacts-root <contracts>/contracts/out \
254
- --output-dir packages/launchpad/src/abi/generated \
255
- --artifact-commit <40-char commit>
352
+ const availableAt = launchpadEmergencyExitAvailableAt(graduation); // undefined unless swept
353
+ const { distributor } = await client.getEmergencyDistributor({ launchToken: token, quoteToken });
354
+ const quote = await client.quoteEmergencyRedeem({ distributor, tokenIn });
256
355
  ```
257
356
 
258
- ## Scope
357
+ Triggering the exit is **owner-only**, unlike both graduation steps — a stranded holder can redeem
358
+ once governance has acted, but cannot pull the lever themselves.
359
+
360
+ The emergency exit is narrower than "a stuck launch": it requires the launch to already be `swept`
361
+ and measures seven days from `sweptAt`, because it exists for the one gap the two-step graduation
362
+ opens. A launch still trading on its curve has nothing to exit from, and there is no expedited path.
363
+
364
+ Redemption divides by the token's **live** `totalSupply()`, re-read every time rather than
365
+ snapshotted. A holder who burns without redeeming therefore raises everyone else's payout per token,
366
+ and the last redeemer takes the entire remainder. Quotes drift with other people's behaviour, so
367
+ re-read immediately before submitting.
368
+
369
+ ## Keeper operations
370
+
371
+ Curve sweeps and the V4 pool's epoch-paged partner settlement live in the keeper surface. Note the
372
+ argument asymmetry that is easy to get wrong: `beginPoolFeeSweep` takes the **token**, while
373
+ `settlePoolFeeSweepPartners` and `finalizePoolFeeSweep` take the **poolId** (resolve it with
374
+ `getLaunchpadPoolId`). There is no protocol limit on partner count or page size — any batch ceiling
375
+ is your own gas budget.
376
+
377
+ RC2 changed how a sweep fails. Hitting the internal price-impact ceiling no longer reverts the
378
+ batch: the hook converts what it can, leaves the rest pending, and carries an unsettled partner
379
+ remainder into the next epoch. `InternalPriceImpactExceeded` is gone, so there is nothing to catch
380
+ and retry — read `getLaunchpadPartnerEpochAccounting({ poolId, epochId })` instead, keep paging
381
+ while `allPartnersSettled` is false, finalize only once `finalizeReady` is true (the hook checks
382
+ five conditions, not just the partner count), and treat
383
+ `partnerLaunchInputConsumed < partnerLaunchInputTotal` as work still outstanding rather than as an
384
+ error.
385
+
386
+ One trap that predates RC2: `MemeHook.settlingEpoch` is stored **one-based**, zero meaning "no
387
+ sweep open". Take the epoch id from `getLaunchpadPoolSweepState(...).settlingEpochId`, which decodes
388
+ it; the raw slot is one too high and makes every settle and finalize revert.
389
+
390
+ A curve `sweepFees` can also graduate a launch as a side effect: its internal buyback may take the
391
+ last sellable tokens. Re-read the graduation status after every successful sweep.
392
+
393
+ ## Errors
259
394
 
260
- Not included: Tax tokens, referrals, bridges, DEX aggregator swaps, protocol-owner entry points,
261
- or V3 LP management. Graduated (post-migration) trading stays quote-only until an audited router
262
- contract exists.
395
+ Every contract revert this deployment can produce maps to a stable, catchable type; the mapping is
396
+ asserted exhaustive against the generated ABIs. The ones worth branching on:
397
+
398
+ - `LaunchpadTradingClosedError` — the curve reached its boundary. Permanent; trade the V4 pool.
399
+ - `LaunchpadTradeFeeConsumesInputError` — fees exceed the input. Increase size or wait for decay.
400
+ - `LaunchpadInsufficientQuoteReserveError` — the sell exceeds the curve's real, non-phantom reserve.
401
+ - `LaunchpadSlippageError`, `LaunchpadDeadlineExpiredError` — re-quote and rebuild.
402
+ - `LaunchpadCreatorDeniedError` vs `LaunchpadLaunchNotPermittedError` — a sanction on one account
403
+ versus a gate closed for everyone not allowlisted. Different remedies.
404
+ - `LaunchpadEconomicsHashMismatchError` — governance moved the template; re-read and rebuild.
405
+ - `LaunchpadApprovalRequiredError` — carries the exact `spender`, which differs by operation.
406
+ - `LaunchpadPhaseUnavailableError` from `prepareGraduate` — under RC2 this usually means someone
407
+ else's trade already graduated the launch. Check `error.status`:
408
+ `launchpadGraduateIsIdempotentSuccess(status)` is true for `swept` and `poolCreated`, which a
409
+ keeper should treat as done rather than alert on.
410
+
411
+ ## Mainnet
412
+
413
+ Same contracts, same economics, one difference left. Mainnet rotated onto the Holder-aware forwarder
414
+ on 2026-09-08 and bound a distributor factory to it, so both creation paths and Holder Fee Sharing
415
+ work exactly as they do on the testnet — the addresses differ, nothing else. What still differs is
416
+ the quote token: USDC is the only one, and tUSDG is a testnet faucet token that leaves nothing
417
+ tradable if carried over.
418
+
419
+ Public launch is open and create, buy and sell have been exercised on-chain; graduation has not.
420
+ Details in the [integration guide](docs/integration-guide.md#mainnet-what-differs).
421
+
422
+ ## Package entry points
423
+
424
+ - `@masterpeach/launchpad-sdk` — clients, actions, math, types and errors
425
+ - `@masterpeach/launchpad-sdk/abi` — generated Production V2 ABIs
426
+ - `@masterpeach/launchpad-sdk/core` — shared chain-agnostic primitives
427
+
428
+ See the [integration guide](docs/integration-guide.md) for the full lifecycle, indexing notes,
429
+ keeper runbooks and a go-live checklist.