@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 +345 -178
- package/README.zh-CN.md +300 -154
- package/dist/abi/index.cjs +20947 -1433
- package/dist/abi/index.cjs.map +1 -1
- package/dist/abi/index.d.cts +16370 -1159
- package/dist/abi/index.d.ts +16370 -1159
- package/dist/abi/index.js +41 -13
- package/dist/chunk-DZMMNDQL.js +22254 -0
- package/dist/chunk-DZMMNDQL.js.map +1 -0
- package/dist/index.cjs +24445 -2739
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2978 -712
- package/dist/index.d.ts +2978 -712
- package/dist/index.js +4465 -1200
- package/dist/index.js.map +1 -1
- package/package.json +18 -17
- package/dist/chunk-SH2Y5F5L.js +0 -2754
- package/dist/chunk-SH2Y5F5L.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @masterpeach/launchpad-sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
60
|
+
chainId: deployment.chainId,
|
|
52
61
|
family: "evm",
|
|
53
|
-
nativeCurrency: { symbol: "
|
|
62
|
+
nativeCurrency: { symbol: "ARC", decimals: 18 },
|
|
54
63
|
rpcUrls: ["https://rpc.testnet.arc.io"],
|
|
55
64
|
explorerUrl: "https://testnet.arcscan.app",
|
|
56
|
-
apiBaseUrl: "https://
|
|
65
|
+
apiBaseUrl: "https://api.testnet.arc.example",
|
|
57
66
|
realtimeUrl: "wss://rpc.testnet.arc.io",
|
|
58
|
-
deployment
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
allowance → `simulateContract` → `writeContract`. Nothing is broadcast without a simulation.
|
|
109
|
+
## Trade
|
|
117
110
|
|
|
118
111
|
```ts
|
|
119
|
-
|
|
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
|
|
122
|
-
const prepared = await client.prepareBuy({
|
|
119
|
+
const buy = await client.prepareBuy({
|
|
123
120
|
token,
|
|
124
|
-
quoteIn,
|
|
125
|
-
slippage: { bps: 100 },
|
|
126
|
-
deadline
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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.
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
320
|
+
const usability = await client.getPartnerUsability(partnerId);
|
|
321
|
+
```
|
|
231
322
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
238
|
-
`
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
349
|
+
```ts
|
|
350
|
+
import { launchpadEmergencyExitAvailableAt } from "@masterpeach/launchpad-sdk";
|
|
246
351
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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.
|