@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/CHANGELOG.md +227 -4
- package/README.md +4 -3
- package/dist/addresses.d.ts +73 -7
- package/dist/addresses.js +96 -5
- package/dist/claims.d.ts +43 -6
- package/dist/claims.js +75 -16
- package/dist/config.d.ts +48 -37
- package/dist/config.js +282 -52
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +20 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +15 -4
- package/dist/jupiter-referral.d.ts +334 -0
- package/dist/jupiter-referral.js +666 -0
- package/dist/launch-metadata.d.ts +5 -1
- package/dist/launch-metadata.js +2 -0
- package/dist/meteora-referral.d.ts +119 -0
- package/dist/meteora-referral.js +188 -0
- package/dist/payments.d.ts +17 -0
- package/dist/payments.js +20 -2
- package/dist/pool.d.ts +108 -17
- package/dist/pool.js +237 -60
- package/dist/position-claims.d.ts +215 -0
- package/dist/position-claims.js +457 -0
- package/dist/priority-fee.d.ts +202 -0
- package/dist/priority-fee.js +322 -0
- package/dist/reads.d.ts +20 -0
- package/dist/reads.js +39 -2
- package/dist/settings.d.ts +73 -0
- package/dist/settings.js +119 -0
- package/dist/swap.d.ts +95 -23
- package/dist/swap.js +144 -30
- package/dist/tx.d.ts +2 -0
- package/dist/tx.js +4 -2
- package/dist/validate.d.ts +14 -1
- package/dist/validate.js +86 -6
- package/package.json +3 -2
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 {
|
|
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
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
|
|
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
|
|
233
|
-
* compute budget, the buyer's WSOL account
|
|
234
|
-
* `createPoolWithFirstBuy` (proven call pattern of
|
|
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
|
|
303
|
+
const head = [];
|
|
264
304
|
if (nonce) {
|
|
265
|
-
|
|
305
|
+
head.push(SystemProgram.nonceAdvance({
|
|
266
306
|
noncePubkey: nonce.nonceAccount,
|
|
267
307
|
authorizedPubkey: nonce.authority,
|
|
268
308
|
}));
|
|
269
309
|
}
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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 {
|
|
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
|
|
515
|
-
* quote, SPL token program for both tokens, `enableFirstSwapWithMinFee
|
|
516
|
-
*
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 {
|
|
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
|
},
|