@forgedevv/core 0.1.0 → 0.2.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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Jupiter referral fee claim (docs/METEORA.md "The platform fee", after graduation; D7, D8).
3
+ *
4
+ * After a coin graduates, trades go through Jupiter and FORGE's 30 bps integrator fee is collected
5
+ * by the Jupiter referral program into token accounts of FORGE's referral account
6
+ * (`FORGE_JUPITER_REFERRAL_ACCOUNT`), one per currency Jupiter collects in. Claiming is
7
+ * permissionless: it moves a token account's balance to the referral account's `partner` (the
8
+ * owner of the referral account: the FORGE multisig vault), minus the project's share, so the
9
+ * fees reach the treasury and feed the buyback (D8). The fee payer only pays the network fee and,
10
+ * the first time, the rent of the partner's token account.
11
+ *
12
+ * Built from the referral program's IDL (program `REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3`,
13
+ * `@jup-ag/referral-sdk` 0.3.0) with raw instructions: the SDK pulls Anchor, too heavy for the
14
+ * transaction core. Two collection accounts exist per mint and both are claimed when funded:
15
+ * - `claim`: the classic referral token account, PDA `["referral_ata", referral account, mint]`;
16
+ * - `claimV2`: the associated token account of the referral account (classic or Token-2022).
17
+ * Account order, PDA seeds and Anchor discriminators are the IDL's; the unit tests pin them.
18
+ *
19
+ * Mainnet only: the referral program and Jupiter's fee collection live on mainnet-beta. The
20
+ * builder checks the connection's genesis hash and refuses any other cluster (`wrong_cluster`).
21
+ * The partner can be asserted (`expectedPartner` = the vault): a referral account owned by
22
+ * anyone else is refused (`partner_mismatch`), fees would not reach the treasury.
23
+ */
24
+ import { createHash } from 'node:crypto';
25
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, unpackAccount, } from '@solana/spl-token';
26
+ import { PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '@solana/web3.js';
27
+ import { PaymentError, assertCluster } from './payments.js';
28
+ import { retryRpc } from './reads.js';
29
+ // ---------------------------------------------------------------------------------------------
30
+ // Constants (IDL)
31
+ // ---------------------------------------------------------------------------------------------
32
+ /** Jupiter referral program (mainnet-beta). */
33
+ export const JUPITER_REFERRAL_PROGRAM_ID = new PublicKey('REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3');
34
+ function anchorDiscriminator(name) {
35
+ return createHash('sha256').update(name).digest().subarray(0, 8);
36
+ }
37
+ export const JUPITER_REFERRAL_ACCOUNT_DISCRIMINATOR = anchorDiscriminator('account:ReferralAccount');
38
+ export const JUPITER_PROJECT_DISCRIMINATOR = anchorDiscriminator('account:Project');
39
+ export const JUPITER_CLAIM_DISCRIMINATOR = anchorDiscriminator('global:claim');
40
+ export const JUPITER_CLAIM_V2_DISCRIMINATOR = anchorDiscriminator('global:claim_v2');
41
+ export const JUPITER_CREATE_ADMIN_TOKEN_ACCOUNT_DISCRIMINATOR = anchorDiscriminator('global:create_admin_token_account');
42
+ export class JupiterReferralError extends Error {
43
+ code;
44
+ constructor(code, message, options) {
45
+ super(message, options);
46
+ this.name = 'JupiterReferralError';
47
+ this.code = code;
48
+ }
49
+ }
50
+ function readString(data, offset) {
51
+ if (data.length < offset + 4)
52
+ throw new RangeError('string length out of bounds');
53
+ const length = data.readUInt32LE(offset);
54
+ const start = offset + 4;
55
+ if (data.length < start + length)
56
+ throw new RangeError('string out of bounds');
57
+ return { value: data.subarray(start, start + length).toString('utf8'), next: start + length };
58
+ }
59
+ /** `ReferralAccount { partner, project, share_bps: u16, name: Option<String> }`. */
60
+ export function decodeJupiterReferralAccount(data) {
61
+ const fail = (reason) => new JupiterReferralError('not_a_referral_account', reason);
62
+ if (data.length < 8 + 32 + 32 + 2 + 1)
63
+ throw fail('referral account data too short');
64
+ if (!data.subarray(0, 8).equals(JUPITER_REFERRAL_ACCOUNT_DISCRIMINATOR)) {
65
+ throw fail('not a Jupiter referral account');
66
+ }
67
+ const partner = new PublicKey(data.subarray(8, 40));
68
+ const project = new PublicKey(data.subarray(40, 72));
69
+ const shareBps = data.readUInt16LE(72);
70
+ const tag = data[74];
71
+ let name = null;
72
+ if (tag === 1) {
73
+ try {
74
+ name = readString(data, 75).value;
75
+ }
76
+ catch (error) {
77
+ throw new JupiterReferralError('not_a_referral_account', 'referral account name invalid', {
78
+ cause: error,
79
+ });
80
+ }
81
+ }
82
+ else if (tag !== 0) {
83
+ throw fail('referral account name tag invalid');
84
+ }
85
+ return { partner, project, shareBps, name };
86
+ }
87
+ /** `Project { base, admin, name: String, default_share_bps: u16 }`. */
88
+ export function decodeJupiterProject(data) {
89
+ const fail = (reason, cause) => new JupiterReferralError('not_a_project', reason, { cause });
90
+ if (data.length < 8 + 32 + 32 + 4 + 2)
91
+ throw fail('project data too short');
92
+ if (!data.subarray(0, 8).equals(JUPITER_PROJECT_DISCRIMINATOR)) {
93
+ throw fail('not a Jupiter referral project');
94
+ }
95
+ const base = new PublicKey(data.subarray(8, 40));
96
+ const admin = new PublicKey(data.subarray(40, 72));
97
+ let name;
98
+ try {
99
+ name = readString(data, 72);
100
+ }
101
+ catch (error) {
102
+ throw fail('project name invalid', error);
103
+ }
104
+ if (data.length < name.next + 2)
105
+ throw fail('project data too short');
106
+ return { base, admin, name: name.value, defaultShareBps: data.readUInt16LE(name.next) };
107
+ }
108
+ // ---------------------------------------------------------------------------------------------
109
+ // Addresses
110
+ // ---------------------------------------------------------------------------------------------
111
+ /** Classic referral token account: PDA `["referral_ata", referral account, mint]`. */
112
+ export function deriveJupiterReferralTokenAccount(referralAccount, mint, programId = JUPITER_REFERRAL_PROGRAM_ID) {
113
+ return PublicKey.findProgramAddressSync([Buffer.from('referral_ata', 'utf8'), referralAccount.toBuffer(), mint.toBuffer()], programId)[0];
114
+ }
115
+ /** V2 referral token account: the associated token account of the referral account. */
116
+ export function deriveJupiterReferralTokenAccountV2(referralAccount, mint, tokenProgram) {
117
+ return getAssociatedTokenAddressSync(mint, referralAccount, true, tokenProgram);
118
+ }
119
+ /** Project authority: PDA `["project_authority", project.base]`. */
120
+ export function deriveJupiterProjectAuthority(base, programId = JUPITER_REFERRAL_PROGRAM_ID) {
121
+ return PublicKey.findProgramAddressSync([Buffer.from('project_authority', 'utf8'), base.toBuffer()], programId)[0];
122
+ }
123
+ const ro = (pubkey) => ({ pubkey, isSigner: false, isWritable: false });
124
+ const rw = (pubkey) => ({ pubkey, isSigner: false, isWritable: true });
125
+ function assertTokenProgram(id) {
126
+ if (!id.equals(TOKEN_PROGRAM_ID) && !id.equals(TOKEN_2022_PROGRAM_ID)) {
127
+ throw new JupiterReferralError('invalid_params', 'tokenProgram is not a token program');
128
+ }
129
+ }
130
+ /**
131
+ * `claim` (v1) or `claim_v2`: the IDL's accounts in the IDL's order. The two differ only in the
132
+ * position of the program accounts at the end (v1: associated token, system, token; v2: system,
133
+ * token, associated token). No arguments.
134
+ */
135
+ export function buildJupiterReferralClaimIx(accounts, version, programId = JUPITER_REFERRAL_PROGRAM_ID) {
136
+ assertTokenProgram(accounts.tokenProgram);
137
+ if (version !== 'v1' && version !== 'v2') {
138
+ throw new JupiterReferralError('invalid_params', 'version must be v1 or v2');
139
+ }
140
+ const shared = [
141
+ { pubkey: accounts.payer, isSigner: true, isWritable: true },
142
+ ro(accounts.project),
143
+ ro(accounts.admin),
144
+ rw(accounts.projectAdminTokenAccount),
145
+ ro(accounts.referralAccount),
146
+ rw(accounts.referralTokenAccount),
147
+ ro(accounts.partner),
148
+ rw(accounts.partnerTokenAccount),
149
+ ro(accounts.mint),
150
+ ];
151
+ const programs = version === 'v1'
152
+ ? [ro(ASSOCIATED_TOKEN_PROGRAM_ID), ro(SystemProgram.programId), ro(accounts.tokenProgram)]
153
+ : [ro(SystemProgram.programId), ro(accounts.tokenProgram), ro(ASSOCIATED_TOKEN_PROGRAM_ID)];
154
+ return new TransactionInstruction({
155
+ programId,
156
+ keys: [...shared, ...programs],
157
+ data: Buffer.from(version === 'v1' ? JUPITER_CLAIM_DISCRIMINATOR : JUPITER_CLAIM_V2_DISCRIMINATOR),
158
+ });
159
+ }
160
+ /** `create_admin_token_account`: needed once per mint before the first claim of that mint. */
161
+ export function buildJupiterCreateAdminTokenAccountIx(accounts, programId = JUPITER_REFERRAL_PROGRAM_ID) {
162
+ assertTokenProgram(accounts.tokenProgram);
163
+ return new TransactionInstruction({
164
+ programId,
165
+ keys: [
166
+ ro(accounts.project),
167
+ rw(accounts.projectAuthority),
168
+ ro(accounts.admin),
169
+ rw(accounts.projectAdminTokenAccount),
170
+ ro(accounts.mint),
171
+ ro(SystemProgram.programId),
172
+ ro(accounts.tokenProgram),
173
+ ro(ASSOCIATED_TOKEN_PROGRAM_ID),
174
+ ],
175
+ data: Buffer.from(JUPITER_CREATE_ADMIN_TOKEN_ACCOUNT_DISCRIMINATOR),
176
+ });
177
+ }
178
+ function tokenAmount(address, info, tokenProgram) {
179
+ if (!info || !info.owner.equals(tokenProgram))
180
+ return 0n;
181
+ try {
182
+ const account = unpackAccount(address, info, tokenProgram);
183
+ return account.isFrozen ? 0n : account.amount;
184
+ }
185
+ catch {
186
+ return 0n;
187
+ }
188
+ }
189
+ /**
190
+ * Reads the referral account, its project, the mint and the collection accounts, and builds the
191
+ * claim(s) of every funded collection account of `mint` to the partner's associated token account.
192
+ * Mainnet-beta only (genesis hash). Returns `transaction: null` when nothing is claimable.
193
+ */
194
+ export async function buildJupiterReferralClaimTx(p) {
195
+ const { connection } = p;
196
+ const commitment = p.commitment ?? 'confirmed';
197
+ const programId = p.programId ?? JUPITER_REFERRAL_PROGRAM_ID;
198
+ const retry = p.retry ?? {};
199
+ if (p.payer.equals(p.referralAccount) || p.mint.equals(p.referralAccount)) {
200
+ throw new JupiterReferralError('invalid_params', 'payer, mint and referral account must differ');
201
+ }
202
+ try {
203
+ await assertCluster(connection, 'mainnet-beta', retry);
204
+ }
205
+ catch (error) {
206
+ if (error instanceof PaymentError && error.code === 'wrong_cluster') {
207
+ throw new JupiterReferralError('wrong_cluster', 'Jupiter referral claims only exist on mainnet-beta', { cause: error });
208
+ }
209
+ throw error;
210
+ }
211
+ const [referralInfo, mintInfo] = await retryRpc(() => connection.getMultipleAccountsInfo([p.referralAccount, p.mint], commitment), retry);
212
+ if (!referralInfo) {
213
+ throw new JupiterReferralError('referral_account_not_found', 'referral account not found');
214
+ }
215
+ if (!referralInfo.owner.equals(programId)) {
216
+ throw new JupiterReferralError('not_a_referral_account', 'referral account is not owned by the Jupiter referral program');
217
+ }
218
+ const referral = decodeJupiterReferralAccount(Buffer.from(referralInfo.data));
219
+ if (p.expectedPartner && !referral.partner.equals(p.expectedPartner)) {
220
+ throw new JupiterReferralError('partner_mismatch', 'the referral account partner is not the expected receiver');
221
+ }
222
+ if (!mintInfo)
223
+ throw new JupiterReferralError('mint_not_found', 'mint not found');
224
+ const tokenProgram = mintInfo.owner;
225
+ if (!tokenProgram.equals(TOKEN_PROGRAM_ID) && !tokenProgram.equals(TOKEN_2022_PROGRAM_ID)) {
226
+ throw new JupiterReferralError('not_a_mint', 'mint is not owned by a token program');
227
+ }
228
+ const projectInfo = await retryRpc(() => connection.getAccountInfo(referral.project, commitment), retry);
229
+ if (!projectInfo)
230
+ throw new JupiterReferralError('project_not_found', 'project not found');
231
+ if (!projectInfo.owner.equals(programId)) {
232
+ throw new JupiterReferralError('not_a_project', 'project is not owned by the referral program');
233
+ }
234
+ const project = decodeJupiterProject(Buffer.from(projectInfo.data));
235
+ const v1Account = deriveJupiterReferralTokenAccount(p.referralAccount, p.mint, programId);
236
+ const v2Account = deriveJupiterReferralTokenAccountV2(p.referralAccount, p.mint, tokenProgram);
237
+ const partnerTokenAccount = getAssociatedTokenAddressSync(p.mint, referral.partner, true, tokenProgram);
238
+ const adminTokenAccount = getAssociatedTokenAddressSync(p.mint, project.admin, true, tokenProgram);
239
+ const [v1Info, v2Info, partnerInfo, adminInfo] = await retryRpc(() => connection.getMultipleAccountsInfo([v1Account, v2Account, partnerTokenAccount, adminTokenAccount], commitment), retry);
240
+ const claims = [];
241
+ const v1Amount = tokenAmount(v1Account, v1Info ?? null, tokenProgram);
242
+ if (v1Amount > 0n)
243
+ claims.push({ version: 'v1', referralTokenAccount: v1Account, amount: v1Amount });
244
+ const v2Amount = tokenAmount(v2Account, v2Info ?? null, tokenProgram);
245
+ if (v2Amount > 0n)
246
+ claims.push({ version: 'v2', referralTokenAccount: v2Account, amount: v2Amount });
247
+ const projectAuthority = deriveJupiterProjectAuthority(project.base, programId);
248
+ const summary = {
249
+ referralAccount: p.referralAccount,
250
+ partner: referral.partner,
251
+ partnerTokenAccount,
252
+ project: referral.project,
253
+ admin: project.admin,
254
+ projectAuthority,
255
+ mint: p.mint,
256
+ tokenProgram,
257
+ claims,
258
+ createsPartnerTokenAccount: claims.length > 0 && !partnerInfo,
259
+ createsAdminTokenAccount: claims.length > 0 && !adminInfo,
260
+ };
261
+ if (claims.length === 0)
262
+ return { transaction: null, lastValidBlockHeight: null, summary };
263
+ const transaction = new Transaction();
264
+ if (summary.createsPartnerTokenAccount) {
265
+ transaction.add(createAssociatedTokenAccountIdempotentInstruction(p.payer, partnerTokenAccount, referral.partner, p.mint, tokenProgram));
266
+ }
267
+ if (summary.createsAdminTokenAccount) {
268
+ transaction.add(buildJupiterCreateAdminTokenAccountIx({
269
+ project: referral.project,
270
+ projectAuthority,
271
+ admin: project.admin,
272
+ projectAdminTokenAccount: adminTokenAccount,
273
+ mint: p.mint,
274
+ tokenProgram,
275
+ }, programId));
276
+ }
277
+ for (const claim of claims) {
278
+ transaction.add(buildJupiterReferralClaimIx({
279
+ payer: p.payer,
280
+ project: referral.project,
281
+ admin: project.admin,
282
+ projectAdminTokenAccount: adminTokenAccount,
283
+ referralAccount: p.referralAccount,
284
+ referralTokenAccount: claim.referralTokenAccount,
285
+ partner: referral.partner,
286
+ partnerTokenAccount,
287
+ mint: p.mint,
288
+ tokenProgram,
289
+ }, claim.version, programId));
290
+ }
291
+ const latest = await retryRpc(() => connection.getLatestBlockhash(commitment), retry);
292
+ transaction.feePayer = p.payer;
293
+ transaction.recentBlockhash = latest.blockhash;
294
+ return { transaction, lastValidBlockHeight: latest.lastValidBlockHeight, summary };
295
+ }
@@ -15,7 +15,9 @@
15
15
  import type { LaunchpadSpec } from '@forge/shared';
16
16
  export type LaunchCoinErrorCode = 'INVALID_INPUT' | 'CONFIG_NOT_FOUND' | 'CONFIG_MISMATCH' | 'POOL_EXISTS' | 'MINT_EXISTS'
17
17
  /** A referral was given for the first buy: it would lose the first-swap minimum fee. */
18
- | 'REFERRAL_UNSUPPORTED' | 'RENT_PAYER_REFUSED' | 'FIRST_BUY_EXCEEDS_CURVE' | 'QUOTE_FAILED' | 'UNEXPECTED_SIGNERS' | 'TX_TOO_LARGE';
18
+ | 'REFERRAL_UNSUPPORTED' | 'RENT_PAYER_REFUSED' | 'FIRST_BUY_EXCEEDS_CURVE' | 'QUOTE_FAILED' | 'UNEXPECTED_SIGNERS' | 'TX_TOO_LARGE'
19
+ /** FORGE_PLATFORM_FEE_WALLET does not exist or is not rent-exempt (user coin launch). */
20
+ | 'PLATFORM_FEE_WALLET_NOT_READY';
19
21
  /** The launch transaction cannot be built. Nothing has been sent. */
20
22
  export declare class LaunchCoinError extends Error {
21
23
  readonly code: LaunchCoinErrorCode;
package/dist/pool.d.ts CHANGED
@@ -33,12 +33,17 @@
33
33
  *
34
34
  * `buildUserCoinLaunchTx` is the same launch for a coin created by a user on the client's
35
35
  * launchpad config: the user is pool creator, rent payer, fee payer and buyer, the mint is the
36
- * only other signer.
36
+ * only other signer. Its first buy pays the FORGE platform fee (0.3%, rounded up) like any swap
37
+ * built by core: one SOL transfer to `FORGE_PLATFORM_FEE_WALLET`, the last instruction.
38
+ *
39
+ * Both builders compare the on-chain config with what core builds for it, field by field
40
+ * (`onchainConfigMismatches` in config.ts), before building anything.
37
41
  */
38
42
  import { type PoolConfig, type SwapQuoteConfig } from '@meteora-ag/dynamic-bonding-curve-sdk';
39
43
  import { PublicKey, Transaction, type Connection } from '@solana/web3.js';
40
- import type { OwnerTransactionSummary } from '@forge/shared';
41
- import type { AddressEnv } from './addresses.js';
44
+ import type { LaunchpadSpec, OwnerTransactionSummary } from '@forge/shared';
45
+ import { type AddressEnv } from './addresses.js';
46
+ import { type OnchainConfigExpectation } from './config.js';
42
47
  export { LAUNCH_METADATA_BUDGET_BYTES, LaunchCoinError, MAX_FIRST_BUY_LAMPORTS, METADATA_LIMITS, assertFirstBuyLamports, assertLaunchMetadata, assertSpecLaunchable, launchMetadataBytes, type LaunchCoinErrorCode, } from './launch-metadata.js';
43
48
  /**
44
49
  * Compute unit limit of the launch transaction: 190,840 CU measured on devnet with the durable
@@ -96,6 +101,12 @@ export interface BuildLaunchCoinTxParams {
96
101
  slippageBps?: number;
97
102
  /** Durable nonce: the transaction then stays valid until the nonce advances. */
98
103
  durableNonce?: DurableNonceParam;
104
+ /**
105
+ * The launchpad spec the coin config was built from. When given, the on-chain config must be
106
+ * exactly `buildLaunchpadCoinConfig(spec)` (`assertLaunchCoinConfig`); without it, its trading
107
+ * fee and anti-sniper flag are only bounded. Recommended.
108
+ */
109
+ spec?: LaunchpadSpec;
99
110
  /**
100
111
  * Who pays the pool, mint, metadata and vault accounts and the pool creation fee (default
101
112
  * `owner`; `creator` only on a config whose pool creation fee is 0).
@@ -137,6 +148,11 @@ export interface LaunchCoinCosts {
137
148
  ownerTemporaryLamports: bigint;
138
149
  /** Signature fees + priority fee (paid by the owner, the fee payer). */
139
150
  networkFeeLamports: bigint;
151
+ /**
152
+ * FORGE platform fee on the first buy (`PLATFORM_FEE_BPS` of it, rounded up), paid by the owner
153
+ * to `FORGE_PLATFORM_FEE_WALLET`. Only set by `buildUserCoinLaunchTx`.
154
+ */
155
+ platformFeeLamports?: bigint;
140
156
  /** `cluster` = `getFeeForMessage`; `estimate` = signatures x 5000 + priority fee. */
141
157
  networkFeeSource: 'cluster' | 'estimate';
142
158
  /** Everything the owner's balance decreases by. */
@@ -193,13 +209,21 @@ export interface LaunchCoinTx {
193
209
  }
194
210
  /**
195
211
  * Checks that an on-chain config is the launchpad coin config of `owner` and that it is safe to
196
- * launch the coin on it: SOL quote, SPL token, fee claimer = owner, FORGE creator share, creator
197
- * locked liquidity, first swap at the minimum fee (otherwise the client's first buy would pay
198
- * the anti-sniper fee), fee scheduler base fee, DAMM v2 migration at exactly 10 SOL.
212
+ * launch the coin on it. Besides the essential checks (SOL quote, SPL token, first swap at the
213
+ * minimum fee, fee scheduler, DAMM v2 at exactly 10 SOL), EVERY field is compared with what core
214
+ * builds for a launchpad coin config (`onchainConfigMismatches`): fee claimer and leftover
215
+ * receiver = owner, FORGE creator share, the 75 / 25 permanently locked liquidity, no pool
216
+ * creation fee, immutable metadata, the exact fee schedule, no migration fee, no locked vesting,
217
+ * FORGE's supply and curve, timestamp activation.
218
+ *
219
+ * With `spec` (the spec the config was built from), the reference is exactly
220
+ * `buildLaunchpadCoinConfig(spec)`; without it, the trading fee and the anti-sniper flag are read
221
+ * from the config and only have to be values a FORGE spec can hold (linear schedule, whole fee of
222
+ * 50..200 bps). Pass the spec whenever it is known.
199
223
  *
200
224
  * @throws LaunchCoinError `CONFIG_MISMATCH` listing every mismatch.
201
225
  */
202
- export declare function assertLaunchCoinConfig(config: PoolConfig, owner: PublicKey): void;
226
+ export declare function assertLaunchCoinConfig(config: PoolConfig, owner: PublicKey, spec?: LaunchpadSpec): void;
203
227
  /** The quote inputs of a pool config, for a pool that does not exist yet. */
204
228
  export declare function quoteConfigFromPoolConfig(config: PoolConfig): SwapQuoteConfig;
205
229
  /**
@@ -229,10 +253,25 @@ export interface BuildUserCoinLaunchTxParams {
229
253
  /** Slippage on the first buy (default 100 bps, at most 1000). */
230
254
  slippageBps?: number;
231
255
  /**
232
- * Accepted for symmetry with `buildLaunchCoinTx`; not read: a user coin launch involves no
233
- * FORGE address (no referral on the first buy, no platform fee on the creation).
256
+ * Environment to read `FORGE_PLATFORM_FEE_WALLET` from (defaults to `process.env`; a browser
257
+ * passes its public copy explicitly). Read only when a platform fee is charged.
234
258
  */
235
259
  env?: AddressEnv;
260
+ /**
261
+ * Charge the FORGE platform fee on the first buy (default `true`): `PLATFORM_FEE_BPS` (30 bps)
262
+ * of `firstBuyLamports`, rounded up to the lamport (`platformFeeLamports`), as ONE SystemProgram
263
+ * transfer from the wallet to `FORGE_PLATFORM_FEE_WALLET`, the last instruction. Nothing is
264
+ * added when there is no first buy. `false` only for tools that must reproduce a launch without
265
+ * it.
266
+ */
267
+ chargePlatformFee?: boolean;
268
+ /**
269
+ * What the on-chain launchpad config must be, compared field by field
270
+ * (`assertUserCoinConfig`): the owner wallet (fee claimer and leftover receiver), the pool
271
+ * creation fee, or the full spec. Without it, every fixed FORGE setting is still compared and
272
+ * the spec fields only have to be within the client bounds.
273
+ */
274
+ expectedConfig?: OnchainConfigExpectation;
236
275
  computeUnitLimit?: number;
237
276
  /** Priority fee, micro-lamports per compute unit (default 0). */
238
277
  computeUnitPriceMicroLamports?: number;
@@ -252,10 +291,14 @@ export interface UserCoinLaunchSummary {
252
291
  expectedTokensOut: bigint;
253
292
  minimumTokensOut: bigint;
254
293
  slippageBps: number;
294
+ /** FORGE platform fee transferred by the wallet (0 without a first buy or when not charged). */
295
+ platformFeeLamports: bigint;
296
+ /** Recipient of the platform fee transfer, or null when there is none. */
297
+ platformFeeWallet: PublicKey | null;
255
298
  /**
256
299
  * Same figures as `LaunchCoinCosts`, read the same way: the `owner*` fields are the wallet's
257
- * (it pays everything, including the pool accounts and the pool creation fee);
258
- * `creatorTotalLamports` is 0 because no separate creator wallet pays anything.
300
+ * (it pays everything, including the pool accounts, the pool creation fee and the platform
301
+ * fee); `creatorTotalLamports` is 0 because no separate creator wallet pays anything.
259
302
  */
260
303
  costs: LaunchCoinCosts;
261
304
  recentBlockhash: string;
@@ -272,20 +315,28 @@ export interface UserCoinLaunchTx {
272
315
  summary: UserCoinLaunchSummary;
273
316
  }
274
317
  /**
275
- * Checks that an on-chain config can launch a user coin with a first buy at the minimum fee: SOL
276
- * quote, SPL token program for both tokens, `enableFirstSwapWithMinFee`. Nothing about who owns
277
- * the config or the creator share: the template checks the config address itself.
318
+ * Checks that an on-chain config is a FORGE launchpad config a user coin can be launched on:
319
+ * the essential checks (SOL quote, SPL token program for both tokens, `enableFirstSwapWithMinFee`,
320
+ * fee scheduler, DAMM v2 at exactly 10 SOL), then EVERY field compared with what core builds for
321
+ * a launchpad config (`onchainConfigMismatches`): liquidity split following the creator share,
322
+ * immutable metadata, the exact fee schedule, no migration fee, no locked vesting, FORGE's supply
323
+ * and curve, timestamp activation. `expected` pins the owner wallet (fee claimer and leftover
324
+ * receiver), the pool creation fee, or the whole spec; without it the spec fields only have to be
325
+ * within the client bounds and the leftover receiver must be the fee claimer.
278
326
  *
279
327
  * @throws LaunchCoinError `CONFIG_MISMATCH` listing every mismatch.
280
328
  */
281
- export declare function assertUserCoinConfig(config: PoolConfig): void;
329
+ export declare function assertUserCoinConfig(config: PoolConfig, expected?: OnchainConfigExpectation): void;
282
330
  /**
283
331
  * Builds the launch transaction of a coin created by a user on the client's launchpad config:
284
332
  * pool creation + the user's first buy at the minimum fee (no referral), with a compute unit limit
285
333
  * (and a priority fee when given) and a recent blockhash. The user is pool creator, rent payer,
286
- * fee payer and buyer; the mint is the only other signer. Rent is read from the cluster.
334
+ * fee payer and buyer; the mint is the only other signer. Rent is read from the cluster. With a
335
+ * first buy, the last instruction transfers the platform fee (`platformFeeLamports(firstBuy)`) to
336
+ * `FORGE_PLATFORM_FEE_WALLET` (see `chargePlatformFee`).
287
337
  *
288
- * @throws LaunchCoinError (typed `code`).
338
+ * @throws LaunchCoinError (typed `code`; `PLATFORM_FEE_WALLET_NOT_READY` when the fee wallet is
339
+ * not rent-exempt), or ForgeAddressError when `FORGE_PLATFORM_FEE_WALLET` is missing.
289
340
  */
290
341
  export declare function buildUserCoinLaunchTx(params: BuildUserCoinLaunchTxParams): Promise<UserCoinLaunchTx>;
291
342
  /**