@forgedevv/core 0.1.0 → 0.3.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/dist/pool.js CHANGED
@@ -33,15 +33,32 @@
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.
41
+ *
42
+ * Priority fee (`priority` setting, priority-fee.ts): the price is read from the recent fees of
43
+ * the launch's writable accounts and bounded by the setting. The compute budget instructions come
44
+ * right after the nonce advance: `setComputeUnitLimit` then `setComputeUnitPrice`. The launchpad
45
+ * coin launch is tight (`LAUNCH_METADATA_BUDGET_BYTES`): when both do not fit in 1,232 bytes, the
46
+ * price goes alone (4 bytes more than the limit it replaces; the fee is then bounded for
47
+ * `MAX_TRANSACTION_COMPUTE_UNITS`, the most the runtime can apply without a limit), and when even
48
+ * that does not fit, the launch keeps its shape without a price (`priorityFeePlacement`
49
+ * `omitted_for_size`): the metadata budget accepted at `/v1/configs` never shrinks. That launch
50
+ * uses a durable nonce, so it stays valid until it lands. The user coin launch always fits both.
37
51
  */
38
52
  import { DYNAMIC_BONDING_CURVE_PROGRAM_ID, DynamicBondingCurveClient, MigrationOption, TokenType, deriveDbcPoolAddress, } from '@meteora-ag/dynamic-bonding-curve-sdk';
39
53
  import { ACCOUNT_SIZE, ASSOCIATED_TOKEN_PROGRAM_ID, MINT_SIZE, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, } from '@solana/spl-token';
40
54
  import { PublicKey, SystemProgram, Transaction, } from '@solana/web3.js';
41
55
  import BN from 'bn.js';
42
- import { quoteFirstBuyOnConfig } from './config.js';
56
+ import { getPlatformFeeWallet } from './addresses.js';
57
+ import { onchainConfigMismatches, quoteFirstBuyOnConfig, } from './config.js';
43
58
  import { FEES, MIGRATION_THRESHOLD_LAMPORTS, SOL_QUOTE_MINT } from './constants.js';
59
+ import { PLATFORM_FEE_BPS, platformFeeLamports } from './swap.js';
44
60
  import { LaunchCoinError, assertFirstBuyLamports, assertLaunchMetadata, } from './launch-metadata.js';
61
+ import { MAX_TRANSACTION_COMPUTE_UNITS, computePriorityFee, fixedPriorityFee, priorityFeeInstructions, readRecentPriorityFees, resolvePriorityFeePolicy, writableAccountsOf, } from './priority-fee.js';
45
62
  import { MAX_TRANSACTION_BYTES, computeBudgetInstructions, priorityFeeLamports, readNonce, requiredSigners, transactionSize, } from './tx.js';
46
63
  // The metadata and first buy checks live in launch-metadata.ts (shared with config.ts); they
47
64
  // stay part of this module's public surface.
@@ -84,14 +101,12 @@ function assertDistinctKeys(entries) {
84
101
  }
85
102
  }
86
103
  /**
87
- * Checks that an on-chain config is the launchpad coin config of `owner` and that it is safe to
88
- * launch the coin on it: SOL quote, SPL token, fee claimer = owner, FORGE creator share, creator
89
- * locked liquidity, first swap at the minimum fee (otherwise the client's first buy would pay
90
- * the anti-sniper fee), fee scheduler base fee, DAMM v2 migration at exactly 10 SOL.
91
- *
92
- * @throws LaunchCoinError `CONFIG_MISMATCH` listing every mismatch.
104
+ * The essential checks of a config a coin is launched on, with readable messages: SOL quote (SPL),
105
+ * SPL base token, first swap at the minimum fee (otherwise the first buy pays the anti-sniper
106
+ * fee), a fee scheduler base fee, DAMM v2 migration at exactly 10 SOL. The full field by field
107
+ * comparison (`onchainConfigMismatches`) follows them.
93
108
  */
94
- export function assertLaunchCoinConfig(config, owner) {
109
+ function essentialConfigProblems(config) {
95
110
  const problems = [];
96
111
  if (!config.quoteMint.equals(SOL_QUOTE_MINT))
97
112
  problems.push('quote mint is not SOL');
@@ -99,16 +114,6 @@ export function assertLaunchCoinConfig(config, owner) {
99
114
  problems.push('quote token is not an SPL token');
100
115
  if (config.tokenType !== TokenType.SPLToken)
101
116
  problems.push('base token type is not SPL token');
102
- if (!config.feeClaimer.equals(owner))
103
- problems.push('fee claimer is not the owner wallet');
104
- if (!config.leftoverReceiver.equals(owner))
105
- problems.push('leftover receiver is not the owner');
106
- if (config.creatorTradingFeePercentage !== FEES.forgeCreatorSharePct) {
107
- problems.push(`creator trading fee is not ${FEES.forgeCreatorSharePct}%`);
108
- }
109
- if (!(config.creatorPermanentLockedLiquidityPercentage > 0)) {
110
- problems.push('creator has no permanently locked liquidity');
111
- }
112
117
  if (config.enableFirstSwapWithMinFee !== 1) {
113
118
  problems.push('first swap with minimum fee is disabled (the first buy would pay the anti-sniper fee)');
114
119
  }
@@ -121,6 +126,40 @@ export function assertLaunchCoinConfig(config, owner) {
121
126
  if (BigInt(config.migrationQuoteThreshold.toString()) !== MIGRATION_THRESHOLD_LAMPORTS) {
122
127
  problems.push(`migration threshold is not ${MIGRATION_THRESHOLD_LAMPORTS} lamports`);
123
128
  }
129
+ return problems;
130
+ }
131
+ /**
132
+ * Checks that an on-chain config is the launchpad coin config of `owner` and that it is safe to
133
+ * launch the coin on it. Besides the essential checks (SOL quote, SPL token, first swap at the
134
+ * minimum fee, fee scheduler, DAMM v2 at exactly 10 SOL), EVERY field is compared with what core
135
+ * builds for a launchpad coin config (`onchainConfigMismatches`): fee claimer and leftover
136
+ * receiver = owner, FORGE creator share, the 75 / 25 permanently locked liquidity, no pool
137
+ * creation fee, immutable metadata, the exact fee schedule, no migration fee, no locked vesting,
138
+ * FORGE's supply and curve, timestamp activation.
139
+ *
140
+ * With `spec` (the spec the config was built from), the reference is exactly
141
+ * `buildLaunchpadCoinConfig(spec)`; without it, the trading fee and the anti-sniper flag are read
142
+ * from the config and only have to be values a FORGE spec can hold (linear schedule, whole fee of
143
+ * 50..200 bps). Pass the spec whenever it is known.
144
+ *
145
+ * @throws LaunchCoinError `CONFIG_MISMATCH` listing every mismatch.
146
+ */
147
+ export function assertLaunchCoinConfig(config, owner, spec) {
148
+ const problems = essentialConfigProblems(config);
149
+ if (!config.feeClaimer.equals(owner))
150
+ problems.push('fee claimer is not the owner wallet');
151
+ if (!config.leftoverReceiver.equals(owner))
152
+ problems.push('leftover receiver is not the owner');
153
+ if (config.creatorTradingFeePercentage !== FEES.forgeCreatorSharePct) {
154
+ problems.push(`creator trading fee is not ${FEES.forgeCreatorSharePct}%`);
155
+ }
156
+ if (!(config.creatorPermanentLockedLiquidityPercentage > 0)) {
157
+ problems.push('creator has no permanently locked liquidity');
158
+ }
159
+ problems.push(...onchainConfigMismatches(config, 'launchpadCoin', {
160
+ owner,
161
+ ...(spec !== undefined ? { spec } : {}),
162
+ }));
124
163
  if (problems.length > 0) {
125
164
  fail('CONFIG_MISMATCH', `config is not a valid launchpad coin config: ${problems.join('; ')}`);
126
165
  }
@@ -229,9 +268,10 @@ async function loadLaunchState(connection, accounts, checkConfig) {
229
268
  }
230
269
  /**
231
270
  * Quotes the first buy on the fresh pool at the minimum fee (exact: nothing else can trade before
232
- * it, the pool is created in the same transaction) and assembles the transaction: nonce advance,
233
- * compute budget, the buyer's WSOL account (idempotent), then the SDK instructions of
234
- * `createPoolWithFirstBuy` (proven call pattern of devnet test 3), without referral.
271
+ * it, the pool is created in the same transaction) and returns how to compose the transaction for
272
+ * given compute budget instructions: nonce advance, compute budget, the buyer's WSOL account
273
+ * (idempotent), then the SDK instructions of `createPoolWithFirstBuy` (proven call pattern of
274
+ * devnet test 3), without referral, then the trailing instructions.
235
275
  */
236
276
  async function assembleLaunch(input) {
237
277
  const { buyer, nonce } = input;
@@ -260,26 +300,98 @@ async function assembleLaunch(input) {
260
300
  }
261
301
  : {}),
262
302
  });
263
- const instructions = [];
303
+ const head = [];
264
304
  if (nonce) {
265
- instructions.push(SystemProgram.nonceAdvance({
305
+ head.push(SystemProgram.nonceAdvance({
266
306
  noncePubkey: nonce.nonceAccount,
267
307
  authorizedPubkey: nonce.authority,
268
308
  }));
269
309
  }
270
- instructions.push(...computeBudgetInstructions(input.budget));
310
+ const body = [];
271
311
  // The SDK adds the buyer's WSOL account creation only when it is missing at build time.
272
312
  const buyerWsol = getAssociatedTokenAddressSync(NATIVE_MINT, buyer, true, TOKEN_PROGRAM_ID);
273
313
  if (quote && !createsAssociatedAccount(sdkTx.instructions, buyerWsol)) {
274
- instructions.push(createAssociatedTokenAccountIdempotentInstruction(buyer, buyerWsol, buyer, NATIVE_MINT, TOKEN_PROGRAM_ID));
314
+ body.push(createAssociatedTokenAccountIdempotentInstruction(buyer, buyerWsol, buyer, NATIVE_MINT, TOKEN_PROGRAM_ID));
315
+ }
316
+ body.push(...sdkTx.instructions, ...(input.trailingInstructions ?? []));
317
+ const compose = (budget) => {
318
+ const transaction = new Transaction({
319
+ feePayer: buyer,
320
+ recentBlockhash: input.recentBlockhash,
321
+ });
322
+ if (input.lastValidBlockHeight !== null) {
323
+ transaction.lastValidBlockHeight = input.lastValidBlockHeight;
324
+ }
325
+ transaction.add(...head, ...budget, ...body);
326
+ return { transaction, txBytes: transactionSize(transaction) };
327
+ };
328
+ return { quote, compose };
329
+ }
330
+ /**
331
+ * The compute budget of a launch: the legacy shape (limit, fixed price if any) without a
332
+ * `priority` setting; otherwise the priced shape that fits (see the top of this file).
333
+ * `allowPriceOnly`: the price may go without the limit (launchpad coin launch only).
334
+ */
335
+ async function priceLaunch(input) {
336
+ const { compose, budget } = input;
337
+ const legacy = compose(computeBudgetInstructions(budget));
338
+ const fixed = fixedPriorityFee(budget.units, budget.microLamports);
339
+ if (input.priority === undefined) {
340
+ return { ...legacy, priorityFee: fixed, placement: 'limit_and_price', feeBudget: budget };
341
+ }
342
+ let recentFees;
343
+ try {
344
+ recentFees = await readRecentPriorityFees(input.connection, writableAccountsOf(legacy.transaction.instructions));
345
+ }
346
+ catch {
347
+ // Degraded, reported in the quote (`rpc_unavailable`): the floor still prices the launch.
348
+ recentFees = null;
349
+ }
350
+ const full = computePriorityFee({
351
+ recentFees,
352
+ computeUnitLimit: budget.units,
353
+ policy: input.priority,
354
+ });
355
+ const withLimit = compose(priorityFeeInstructions(full));
356
+ if (withLimit.txBytes <= MAX_TRANSACTION_BYTES) {
357
+ return {
358
+ ...withLimit,
359
+ priorityFee: full,
360
+ placement: 'limit_and_price',
361
+ feeBudget: { units: full.computeUnitLimit, microLamports: full.microLamports },
362
+ };
363
+ }
364
+ if (input.allowPriceOnly) {
365
+ const alone = computePriorityFee({
366
+ recentFees,
367
+ computeUnitLimit: MAX_TRANSACTION_COMPUTE_UNITS,
368
+ policy: input.priority,
369
+ });
370
+ const priceOnly = compose(priorityFeeInstructions(alone, { priceOnly: true }));
371
+ if (priceOnly.txBytes <= MAX_TRANSACTION_BYTES) {
372
+ return {
373
+ ...priceOnly,
374
+ priorityFee: alone,
375
+ placement: 'price_only',
376
+ feeBudget: { units: alone.computeUnitLimit, microLamports: alone.microLamports },
377
+ };
378
+ }
379
+ }
380
+ return { ...legacy, priorityFee: fixed, placement: 'omitted_for_size', feeBudget: budget };
381
+ }
382
+ /** @throws LaunchCoinError `INVALID_INPUT` on a bad setting or one given with a fixed price. */
383
+ function assertPrioritySetting(priority, computeUnitPriceMicroLamports) {
384
+ if (priority === undefined)
385
+ return;
386
+ if (computeUnitPriceMicroLamports !== undefined) {
387
+ fail('INVALID_INPUT', 'pass either priority or computeUnitPriceMicroLamports, not both');
388
+ }
389
+ try {
390
+ resolvePriorityFeePolicy(priority);
275
391
  }
276
- instructions.push(...sdkTx.instructions);
277
- const transaction = new Transaction({ feePayer: buyer, recentBlockhash: input.recentBlockhash });
278
- if (input.lastValidBlockHeight !== null) {
279
- transaction.lastValidBlockHeight = input.lastValidBlockHeight;
392
+ catch (error) {
393
+ fail('INVALID_INPUT', `priority: ${error instanceof Error ? error.message : String(error)}`);
280
394
  }
281
- transaction.add(...instructions);
282
- return { transaction, quote, txBytes: transactionSize(transaction) };
283
395
  }
284
396
  /** The required signers, and whether they are exactly `expected` with `feePayer` first. */
285
397
  function checkSigners(transaction, expected, feePayer) {
@@ -332,9 +444,11 @@ async function computeLaunchCosts(input) {
332
444
  const rentPayerCosts = poolAccountsRentLamports +
333
445
  METAPLEX_CREATE_METADATA_FEE_LAMPORTS +
334
446
  input.poolCreationFeeLamports;
447
+ const platformFee = input.platformFeeLamports ?? 0n;
335
448
  const ownerTotalLamports = firstBuy +
336
449
  networkFeeLamports +
337
450
  ownerTokenAccountRentLamports +
451
+ platformFee +
338
452
  (input.rentPayerIsOwner ? rentPayerCosts : 0n);
339
453
  return {
340
454
  firstBuyLamports: firstBuy,
@@ -346,6 +460,7 @@ async function computeLaunchCosts(input) {
346
460
  ownerTemporaryLamports,
347
461
  networkFeeLamports,
348
462
  networkFeeSource,
463
+ ...(input.platformFeeLamports !== undefined ? { platformFeeLamports: platformFee } : {}),
349
464
  ownerTotalLamports,
350
465
  ownerRequiredBalanceLamports: ownerTotalLamports + ownerTemporaryLamports,
351
466
  creatorTotalLamports: input.rentPayerIsOwner ? 0n : rentPayerCosts,
@@ -388,6 +503,7 @@ export async function buildLaunchCoinTx(params) {
388
503
  fail('INVALID_INPUT', "rentPayer must be 'owner' or 'creator'");
389
504
  }
390
505
  assertNoFirstBuyReferral(params.referralTokenAccount);
506
+ assertPrioritySetting(params.priority, params.computeUnitPriceMicroLamports);
391
507
  const keys = [
392
508
  ['config', config],
393
509
  ['creator', creator],
@@ -406,7 +522,7 @@ export async function buildLaunchCoinTx(params) {
406
522
  const pool = deriveDbcPoolAddress(SOL_QUOTE_MINT, mint, config);
407
523
  const rentPayer = rentPayerKind === 'owner' ? owner : creator;
408
524
  // 2. On-chain state: config (this owner's launchpad coin config), pool and mint not existing.
409
- const { poolConfig, client, program } = await loadLaunchState(connection, { config, pool, mint }, (decoded) => assertLaunchCoinConfig(decoded, owner));
525
+ const { poolConfig, client, program } = await loadLaunchState(connection, { config, pool, mint }, (decoded) => assertLaunchCoinConfig(decoded, owner, params.spec));
410
526
  const poolCreationFeeLamports = toBigInt(poolConfig.poolCreationFee);
411
527
  if (rentPayerKind === 'creator' && poolCreationFeeLamports !== 0n) {
412
528
  fail('RENT_PAYER_REFUSED', `the creator wallet cannot pay the rent on a config with a pool creation fee (${poolCreationFeeLamports} lamports): it would pay the client's own fee`);
@@ -426,8 +542,8 @@ export async function buildLaunchCoinTx(params) {
426
542
  recentBlockhash = latest.blockhash;
427
543
  lastValidBlockHeight = latest.lastValidBlockHeight;
428
544
  }
429
- // 4. Quote and instructions.
430
- const { transaction, quote, txBytes } = await assembleLaunch({
545
+ // 4. Quote and instructions, then the compute budget that fits.
546
+ const { quote, compose } = await assembleLaunch({
431
547
  client,
432
548
  poolConfig,
433
549
  config,
@@ -440,11 +556,18 @@ export async function buildLaunchCoinTx(params) {
440
556
  metadataUri: params.metadataUri,
441
557
  firstBuyLamports: params.firstBuyLamports,
442
558
  slippageBps,
443
- budget,
444
559
  nonce,
445
560
  recentBlockhash,
446
561
  lastValidBlockHeight,
447
562
  });
563
+ const priced = await priceLaunch({
564
+ connection,
565
+ compose,
566
+ budget,
567
+ priority: params.priority,
568
+ allowPriceOnly: true,
569
+ });
570
+ const { transaction, txBytes } = priced;
448
571
  // 5. Signers: exactly owner, creator, mint (+ the nonce authority). With the owner paying
449
572
  // the rent, the creator wallet must not be debitable at all (read-only signer).
450
573
  const expected = [owner, creator, mint];
@@ -466,7 +589,7 @@ export async function buildLaunchCoinTx(params) {
466
589
  transaction,
467
590
  signatureCount: signers.actual.length,
468
591
  nonce,
469
- budget,
592
+ budget: priced.feeBudget,
470
593
  quote,
471
594
  firstBuyLamports: params.firstBuyLamports,
472
595
  poolCreationFeeLamports,
@@ -503,7 +626,9 @@ export async function buildLaunchCoinTx(params) {
503
626
  recentBlockhash,
504
627
  lastValidBlockHeight,
505
628
  computeUnitLimit,
506
- computeUnitPriceMicroLamports,
629
+ computeUnitPriceMicroLamports: priced.priorityFee.microLamports,
630
+ priorityFee: priced.priorityFee,
631
+ priorityFeePlacement: priced.placement,
507
632
  signerSideSigners,
508
633
  ownerSigner: owner,
509
634
  txBytes,
@@ -511,34 +636,49 @@ export async function buildLaunchCoinTx(params) {
511
636
  };
512
637
  }
513
638
  /**
514
- * Checks that an on-chain config can launch a user coin with a first buy at the minimum fee: SOL
515
- * quote, SPL token program for both tokens, `enableFirstSwapWithMinFee`. Nothing about who owns
516
- * the config or the creator share: the template checks the config address itself.
639
+ * Checks that an on-chain config is a FORGE launchpad config a user coin can be launched on:
640
+ * the essential checks (SOL quote, SPL token program for both tokens, `enableFirstSwapWithMinFee`,
641
+ * fee scheduler, DAMM v2 at exactly 10 SOL), then EVERY field compared with what core builds for
642
+ * a launchpad config (`onchainConfigMismatches`): liquidity split following the creator share,
643
+ * immutable metadata, the exact fee schedule, no migration fee, no locked vesting, FORGE's supply
644
+ * and curve, timestamp activation. `expected` pins the owner wallet (fee claimer and leftover
645
+ * receiver), the pool creation fee, or the whole spec; without it the spec fields only have to be
646
+ * within the client bounds and the leftover receiver must be the fee claimer.
517
647
  *
518
648
  * @throws LaunchCoinError `CONFIG_MISMATCH` listing every mismatch.
519
649
  */
520
- export function assertUserCoinConfig(config) {
521
- const problems = [];
522
- if (!config.quoteMint.equals(SOL_QUOTE_MINT))
523
- problems.push('quote mint is not SOL');
524
- if (config.quoteTokenFlag !== 0)
525
- problems.push('quote token is not an SPL token');
526
- if (config.tokenType !== TokenType.SPLToken)
527
- problems.push('base token type is not SPL token');
528
- if (config.enableFirstSwapWithMinFee !== 1) {
529
- problems.push('first swap with minimum fee is disabled (the first buy would pay the anti-sniper fee)');
530
- }
650
+ export function assertUserCoinConfig(config, expected = {}) {
651
+ const problems = [
652
+ ...essentialConfigProblems(config),
653
+ ...onchainConfigMismatches(config, 'launchpad', expected),
654
+ ];
531
655
  if (problems.length > 0) {
532
656
  fail('CONFIG_MISMATCH', `config cannot launch a user coin: ${problems.join('; ')}`);
533
657
  }
534
658
  }
659
+ /**
660
+ * The platform fee wallet must exist and be rent-exempt: a SOL transfer to an account below
661
+ * its rent-exempt minimum fails, and the whole launch with it.
662
+ *
663
+ * @throws LaunchCoinError `PLATFORM_FEE_WALLET_NOT_READY`.
664
+ */
665
+ async function assertPlatformFeeWalletLive(connection, wallet) {
666
+ const info = await connection.getAccountInfo(wallet, 'confirmed');
667
+ const minimum = BigInt(await connection.getMinimumBalanceForRentExemption(info?.data.length ?? 0));
668
+ if (!info || BigInt(info.lamports) < minimum) {
669
+ fail('PLATFORM_FEE_WALLET_NOT_READY', 'FORGE_PLATFORM_FEE_WALLET must exist and be rent-exempt, otherwise the launch fails on-chain');
670
+ }
671
+ }
535
672
  /**
536
673
  * Builds the launch transaction of a coin created by a user on the client's launchpad config:
537
674
  * pool creation + the user's first buy at the minimum fee (no referral), with a compute unit limit
538
675
  * (and a priority fee when given) and a recent blockhash. The user is pool creator, rent payer,
539
- * fee payer and buyer; the mint is the only other signer. Rent is read from the cluster.
676
+ * fee payer and buyer; the mint is the only other signer. Rent is read from the cluster. With a
677
+ * first buy, the last instruction transfers the platform fee (`platformFeeLamports(firstBuy)`) to
678
+ * `FORGE_PLATFORM_FEE_WALLET` (see `chargePlatformFee`).
540
679
  *
541
- * @throws LaunchCoinError (typed `code`).
680
+ * @throws LaunchCoinError (typed `code`; `PLATFORM_FEE_WALLET_NOT_READY` when the fee wallet is
681
+ * not rent-exempt), or ForgeAddressError when `FORGE_PLATFORM_FEE_WALLET` is missing.
542
682
  */
543
683
  export async function buildUserCoinLaunchTx(params) {
544
684
  const { connection, config, wallet } = params;
@@ -566,15 +706,29 @@ export async function buildUserCoinLaunchTx(params) {
566
706
  ['wallet', wallet],
567
707
  ['mint', mint],
568
708
  ]);
709
+ assertPrioritySetting(params.priority, params.computeUnitPriceMicroLamports);
569
710
  const baseMint = mint;
570
711
  const pool = deriveDbcPoolAddress(SOL_QUOTE_MINT, baseMint, config);
712
+ // FORGE platform fee on the first buy (rounded up), to FORGE_PLATFORM_FEE_WALLET only.
713
+ const platformFee = params.chargePlatformFee === false || params.firstBuyLamports <= 0n
714
+ ? 0n
715
+ : platformFeeLamports(params.firstBuyLamports, PLATFORM_FEE_BPS);
716
+ const platformFeeWallet = platformFee > 0n ? getPlatformFeeWallet(params.env) : null;
717
+ if (platformFeeWallet &&
718
+ (platformFeeWallet.equals(baseMint) ||
719
+ platformFeeWallet.equals(config) ||
720
+ platformFeeWallet.equals(pool))) {
721
+ fail('INVALID_INPUT', 'FORGE_PLATFORM_FEE_WALLET must not be the mint, the config or the pool');
722
+ }
571
723
  // 2. On-chain state.
572
- const { poolConfig, client, program } = await loadLaunchState(connection, { config, pool, mint: baseMint }, assertUserCoinConfig);
724
+ const { poolConfig, client, program } = await loadLaunchState(connection, { config, pool, mint: baseMint }, (decoded) => assertUserCoinConfig(decoded, params.expectedConfig));
573
725
  const poolCreationFeeLamports = toBigInt(poolConfig.poolCreationFee);
726
+ if (platformFeeWallet)
727
+ await assertPlatformFeeWalletLive(connection, platformFeeWallet);
574
728
  // 3. Recent blockhash: the user signs right away, no durable nonce.
575
729
  const latest = await connection.getLatestBlockhash('confirmed');
576
- // 4. Quote and instructions.
577
- const { transaction, quote, txBytes } = await assembleLaunch({
730
+ // 4. Quote and instructions, then the compute budget.
731
+ const { quote, compose } = await assembleLaunch({
578
732
  client,
579
733
  poolConfig,
580
734
  config,
@@ -587,11 +741,29 @@ export async function buildUserCoinLaunchTx(params) {
587
741
  metadataUri: params.metadataUri,
588
742
  firstBuyLamports: params.firstBuyLamports,
589
743
  slippageBps,
590
- budget,
591
744
  nonce: null,
592
745
  recentBlockhash: latest.blockhash,
593
746
  lastValidBlockHeight: latest.lastValidBlockHeight,
747
+ // Last, like on a swap (docs/METEORA.md, "The platform fee").
748
+ trailingInstructions: platformFeeWallet
749
+ ? [
750
+ SystemProgram.transfer({
751
+ fromPubkey: wallet,
752
+ toPubkey: platformFeeWallet,
753
+ lamports: platformFee,
754
+ }),
755
+ ]
756
+ : [],
594
757
  });
758
+ // The browser verifier wants a limit with any price: never the price alone here.
759
+ const priced = await priceLaunch({
760
+ connection,
761
+ compose,
762
+ budget,
763
+ priority: params.priority,
764
+ allowPriceOnly: false,
765
+ });
766
+ const { transaction, txBytes } = priced;
595
767
  // 5. Signers: exactly the wallet (fee payer, first) and the mint.
596
768
  const signers = checkSigners(transaction, [wallet, baseMint], wallet);
597
769
  if (!signers.ok) {
@@ -606,11 +778,12 @@ export async function buildUserCoinLaunchTx(params) {
606
778
  transaction,
607
779
  signatureCount: signers.actual.length,
608
780
  nonce: null,
609
- budget,
781
+ budget: priced.feeBudget,
610
782
  quote,
611
783
  firstBuyLamports: params.firstBuyLamports,
612
784
  poolCreationFeeLamports,
613
785
  rentPayerIsOwner: true,
786
+ platformFeeLamports: platformFee,
614
787
  });
615
788
  return {
616
789
  transaction,
@@ -626,11 +799,15 @@ export async function buildUserCoinLaunchTx(params) {
626
799
  expectedTokensOut: quote ? quote.tokensOut : 0n,
627
800
  minimumTokensOut: quote ? quote.minimumTokensOut : 0n,
628
801
  slippageBps,
802
+ platformFeeLamports: platformFee,
803
+ platformFeeWallet,
629
804
  costs,
630
805
  recentBlockhash: latest.blockhash,
631
806
  lastValidBlockHeight: latest.lastValidBlockHeight,
632
807
  computeUnitLimit,
633
- computeUnitPriceMicroLamports,
808
+ computeUnitPriceMicroLamports: priced.priorityFee.microLamports,
809
+ priorityFee: priced.priorityFee,
810
+ priorityFeePlacement: priced.placement,
634
811
  signers: signers.actual,
635
812
  txBytes,
636
813
  },