@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.
- package/CHANGELOG.md +88 -5
- package/README.md +4 -3
- package/dist/addresses.d.ts +50 -3
- package/dist/addresses.js +70 -1
- package/dist/claims.d.ts +12 -2
- package/dist/claims.js +29 -11
- package/dist/config.d.ts +40 -37
- package/dist/config.js +265 -47
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/jupiter-referral.d.ts +119 -0
- package/dist/jupiter-referral.js +295 -0
- package/dist/launch-metadata.d.ts +3 -1
- package/dist/pool.d.ts +68 -17
- package/dist/pool.js +112 -39
- package/dist/position-claims.d.ts +207 -0
- package/dist/position-claims.js +440 -0
- package/dist/settings.d.ts +73 -0
- package/dist/settings.js +119 -0
- package/dist/swap.d.ts +70 -17
- package/dist/swap.js +89 -24
- package/dist/validate.d.ts +14 -1
- package/dist/validate.js +86 -6
- package/package.json +3 -2
package/dist/swap.d.ts
CHANGED
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
* Refusals (typed `SwapError`, never a silent fallback):
|
|
28
28
|
* - the pool is migrated or its curve is complete (`POOL_GRADUATED`): the template trades through
|
|
29
29
|
* Jupiter in that case;
|
|
30
|
+
* - the pool's config is not in the caller's `allowedConfigs` (`POOL_NOT_ALLOWED`): a launchpad
|
|
31
|
+
* site only trades its own coins;
|
|
30
32
|
* - the platform fee wallet does not exist or is not rent-exempt
|
|
31
33
|
* (`PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT`): the whole swap would fail on-chain (devnet finding);
|
|
32
|
-
* - the
|
|
34
|
+
* - the platform fee wallet is not exactly `FORGE_PLATFORM_FEE_WALLET`, or not a system account
|
|
35
|
+
* (`PLATFORM_FEE_WALLET_MISMATCH`);
|
|
36
|
+
* - the referral account is not an initialized wrapped SOL token account, or is not held by
|
|
37
|
+
* `FORGE_MULTISIG_VAULT` when the environment has it (`REFERRAL_ACCOUNT_INVALID`);
|
|
33
38
|
* - the pool is not a plain SOL-quoted DBC pool (`UNSUPPORTED_POOL`; transfer-hook pools excluded);
|
|
34
39
|
* - the serialized transaction exceeds 1,232 bytes (`TRANSACTION_TOO_LARGE`).
|
|
35
40
|
*
|
|
@@ -50,8 +55,8 @@ import { type PoolConfig, type VirtualPool } from '@meteora-ag/dynamic-bonding-c
|
|
|
50
55
|
import { type AddressEnv } from './addresses.js';
|
|
51
56
|
/** Platform fee on swaps built by @forge/core, in bps (`FEES.platformFeeBps` from @forge/shared). */
|
|
52
57
|
export declare const PLATFORM_FEE_BPS: number;
|
|
53
|
-
/** Highest slippage tolerance accepted by the builders (
|
|
54
|
-
export declare const MAX_SWAP_SLIPPAGE_BPS =
|
|
58
|
+
/** Highest slippage tolerance accepted by the builders (25%), whether given in bps or as a minimum. */
|
|
59
|
+
export declare const MAX_SWAP_SLIPPAGE_BPS = 2500;
|
|
55
60
|
/**
|
|
56
61
|
* Default compute unit limit. Measured on devnet: ~51,000 CU for a buy (~66,500 when it also creates
|
|
57
62
|
* the coin ATA), ~49,000 CU for a sell; the default leaves about 2x headroom (fee scheduler,
|
|
@@ -66,7 +71,7 @@ export declare const MAX_SWAP_PRIORITY_FEE_MICRO_LAMPORTS = 10000000;
|
|
|
66
71
|
export declare const SWAP_BASE_FEE_LAMPORTS_PER_SIGNATURE = 5000n;
|
|
67
72
|
/** Serialized transaction size limit (`PACKET_DATA_SIZE` = 1,232 bytes). */
|
|
68
73
|
export declare const SWAP_MAX_TRANSACTION_BYTES: number;
|
|
69
|
-
export declare const SWAP_ERROR_CODES: readonly ["INVALID_PARAMS", "POOL_NOT_FOUND", "UNSUPPORTED_POOL", "POOL_GRADUATED", "PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT", "REFERRAL_ACCOUNT_INVALID", "INSUFFICIENT_LIQUIDITY", "QUOTE_FAILED", "AMOUNT_TOO_SMALL", "MIN_AMOUNT_OUT_ABOVE_QUOTE", "SLIPPAGE_TOO_HIGH", "INSUFFICIENT_SOL_BALANCE", "INSUFFICIENT_TOKEN_BALANCE", "TRANSACTION_TOO_LARGE", "RPC_ERROR"];
|
|
74
|
+
export declare const SWAP_ERROR_CODES: readonly ["INVALID_PARAMS", "POOL_NOT_FOUND", "UNSUPPORTED_POOL", "POOL_GRADUATED", "POOL_NOT_ALLOWED", "PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT", "PLATFORM_FEE_WALLET_MISMATCH", "REFERRAL_ACCOUNT_INVALID", "INSUFFICIENT_LIQUIDITY", "QUOTE_FAILED", "AMOUNT_TOO_SMALL", "MIN_AMOUNT_OUT_ABOVE_QUOTE", "SLIPPAGE_TOO_HIGH", "INSUFFICIENT_SOL_BALANCE", "INSUFFICIENT_TOKEN_BALANCE", "TRANSACTION_TOO_LARGE", "RPC_ERROR"];
|
|
70
75
|
export type SwapErrorCode = (typeof SWAP_ERROR_CODES)[number];
|
|
71
76
|
/** Public data attached to a `SwapError` (addresses, amounts as decimal strings). Never secrets. */
|
|
72
77
|
export type SwapErrorDetails = Readonly<Record<string, string | number | boolean>>;
|
|
@@ -270,10 +275,31 @@ export interface SwapContext extends SwapPoolState {
|
|
|
270
275
|
recentBlockhash: string;
|
|
271
276
|
lastValidBlockHeight: number;
|
|
272
277
|
}
|
|
273
|
-
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Throws unless the platform fee wallet exists and holds at least its rent-exempt minimum. With
|
|
280
|
+
* `expectedWallet` (FORGE_PLATFORM_FEE_WALLET, as `buildBuyTx` / `buildSellTx` pass it), the
|
|
281
|
+
* wallet must be exactly that key and a plain system account: lamports sent to an account owned by
|
|
282
|
+
* another program could not be spent by FORGE (`PLATFORM_FEE_WALLET_MISMATCH`).
|
|
283
|
+
*/
|
|
284
|
+
export declare function assertPlatformFeeWalletReady(wallet: PublicKey, account: SwapAccountSnapshot | null, rentExemptMinimum: bigint, expectedWallet?: PublicKey | null): void;
|
|
285
|
+
/**
|
|
286
|
+
* Throws unless the referral account is an initialized wrapped SOL token account (SPL Token).
|
|
287
|
+
* With `expectedOwner` (FORGE_MULTISIG_VAULT, when the environment has it), the token account must
|
|
288
|
+
* be held by that key: otherwise the referral share would be paid to someone else.
|
|
289
|
+
*/
|
|
290
|
+
export declare function assertReferralAccountReady(address: PublicKey, account: SwapAccountSnapshot | null, expectedOwner?: PublicKey | null): void;
|
|
291
|
+
/** The FORGE fee accounts a swap transaction must pay, as the builders read them from the env. */
|
|
292
|
+
export interface SwapFeeAccountExpectations {
|
|
293
|
+
/** The platform fee transfer must go to exactly this key (FORGE_PLATFORM_FEE_WALLET). */
|
|
294
|
+
platformFeeWallet?: PublicKey | null;
|
|
295
|
+
/** The Meteora referral token account must be held by this key (FORGE_MULTISIG_VAULT). */
|
|
296
|
+
referralOwner?: PublicKey | null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Throws `POOL_NOT_ALLOWED` unless the pool's config is one of `allowedConfigs`. Without a list
|
|
300
|
+
* (`undefined`), any config is accepted; an empty list accepts none.
|
|
301
|
+
*/
|
|
302
|
+
export declare function assertPoolConfigAllowed(pool: PublicKey, config: PublicKey, allowedConfigs: readonly (PublicKey | string)[] | undefined): void;
|
|
277
303
|
export interface SwapBuildOptions {
|
|
278
304
|
/** Compute unit limit (default `DEFAULT_SWAP_COMPUTE_UNIT_LIMIT`). */
|
|
279
305
|
computeUnitLimit?: number;
|
|
@@ -321,12 +347,19 @@ export declare function compileSwapTransaction(args: {
|
|
|
321
347
|
sizeBytes: number;
|
|
322
348
|
};
|
|
323
349
|
/**
|
|
324
|
-
* Pure transaction builder: validates the context (pool on its curve, fee accounts ready,
|
|
325
|
-
* quotes, and assembles the instructions in the order documented at
|
|
350
|
+
* Pure transaction builder: validates the context (pool on its curve, fee accounts ready and, with
|
|
351
|
+
* `expected`, exactly FORGE's), quotes, and assembles the instructions in the order documented at
|
|
352
|
+
* the top of this file.
|
|
326
353
|
*/
|
|
327
|
-
export declare function buildSwapTransaction(ctx: SwapContext, request: SwapBuildRequest): BuiltSwapTransaction;
|
|
328
|
-
/**
|
|
329
|
-
|
|
354
|
+
export declare function buildSwapTransaction(ctx: SwapContext, request: SwapBuildRequest, expected?: SwapFeeAccountExpectations): BuiltSwapTransaction;
|
|
355
|
+
/**
|
|
356
|
+
* Reads and decodes a pool and its config, plus the current point. With
|
|
357
|
+
* `options.allowedConfigs`, a pool whose config is not in the list is refused (`POOL_NOT_ALLOWED`)
|
|
358
|
+
* before its config is read.
|
|
359
|
+
*/
|
|
360
|
+
export declare function fetchSwapPoolState(connection: Connection, poolAddress: PublicKey | string, commitment?: Commitment, options?: {
|
|
361
|
+
allowedConfigs?: readonly (PublicKey | string)[];
|
|
362
|
+
}): Promise<SwapPoolState & {
|
|
330
363
|
configAddress: PublicKey;
|
|
331
364
|
}>;
|
|
332
365
|
export interface FetchSwapContextParams {
|
|
@@ -336,22 +369,39 @@ export interface FetchSwapContextParams {
|
|
|
336
369
|
platformFeeWallet: PublicKey;
|
|
337
370
|
referralTokenAccount: PublicKey;
|
|
338
371
|
commitment?: Commitment;
|
|
372
|
+
/** Configs the pool may belong to; anything else is `POOL_NOT_ALLOWED` (default: any config). */
|
|
373
|
+
allowedConfigs?: readonly (PublicKey | string)[];
|
|
339
374
|
}
|
|
340
375
|
/** All network reads of a swap build: pool, config, user accounts, fee accounts, rent, blockhash. */
|
|
341
376
|
export declare function fetchSwapContext(p: FetchSwapContextParams): Promise<SwapContext>;
|
|
342
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* FORGE fee accounts used by the swap, resolved through addresses.ts (never hard-coded), and the
|
|
379
|
+
* multisig vault that must hold the referral account (`null` when the environment does not have
|
|
380
|
+
* `FORGE_MULTISIG_VAULT`: the owner check is then skipped).
|
|
381
|
+
*/
|
|
343
382
|
export declare function swapFeeAddresses(env?: AddressEnv): {
|
|
344
383
|
platformFeeWallet: PublicKey;
|
|
345
384
|
referralTokenAccount: PublicKey;
|
|
385
|
+
multisigVault: PublicKey | null;
|
|
346
386
|
};
|
|
347
387
|
interface SwapCommonParams extends SwapBuildOptions {
|
|
348
388
|
connection: Connection;
|
|
349
389
|
pool: PublicKey | string;
|
|
350
390
|
/** The trader: fee payer, signer and owner of the token accounts. */
|
|
351
391
|
user: PublicKey | string;
|
|
352
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Environment to read the FORGE addresses from (defaults to `process.env`):
|
|
394
|
+
* `FORGE_PLATFORM_FEE_WALLET` and `FORGE_METEORA_REFERRAL_ACCOUNT` (required), and
|
|
395
|
+
* `FORGE_MULTISIG_VAULT` (optional: when set, the referral account must be held by it).
|
|
396
|
+
*/
|
|
353
397
|
env?: AddressEnv;
|
|
354
398
|
commitment?: Commitment;
|
|
399
|
+
/**
|
|
400
|
+
* Configs the pool must belong to (e.g. the two configs of the launchpad whose site builds the
|
|
401
|
+
* swap). The pool's config is read on-chain; any other pool is refused with `POOL_NOT_ALLOWED`.
|
|
402
|
+
* Without it, any SOL-quoted DBC pool is accepted.
|
|
403
|
+
*/
|
|
404
|
+
allowedConfigs?: readonly (PublicKey | string)[];
|
|
355
405
|
}
|
|
356
406
|
export type BuildBuyTxParams = SwapCommonParams & {
|
|
357
407
|
/** SOL into the swap, in lamports. The platform fee is paid on top of it. */
|
|
@@ -372,6 +422,8 @@ export type QuoteSwapParams = {
|
|
|
372
422
|
/** Lamports on a buy, raw coin units on a sell. */
|
|
373
423
|
amountIn: bigint;
|
|
374
424
|
commitment?: Commitment;
|
|
425
|
+
/** Same as on `buildBuyTx`: configs the pool must belong to (`POOL_NOT_ALLOWED` otherwise). */
|
|
426
|
+
allowedConfigs?: readonly (PublicKey | string)[];
|
|
375
427
|
} & (SlippageInput | {
|
|
376
428
|
slippageBps?: undefined;
|
|
377
429
|
minAmountOut?: undefined;
|
|
@@ -382,8 +434,9 @@ export type QuoteSwapParams = {
|
|
|
382
434
|
*/
|
|
383
435
|
export declare function quoteSwap(params: QuoteSwapParams): Promise<SwapQuote>;
|
|
384
436
|
/**
|
|
385
|
-
* Startup / pre-build check of the FORGE fee accounts: the platform fee wallet must be
|
|
386
|
-
* and the referral account must be a wrapped SOL token account
|
|
437
|
+
* Startup / pre-build check of the FORGE fee accounts: the platform fee wallet must be a
|
|
438
|
+
* rent-exempt system account and the referral account must be a wrapped SOL token account, held by
|
|
439
|
+
* `FORGE_MULTISIG_VAULT` when the environment has it. Throws `SwapError`.
|
|
387
440
|
*/
|
|
388
441
|
export declare function assertSwapFeeAccountsReady(params: {
|
|
389
442
|
connection: Connection;
|
package/dist/swap.js
CHANGED
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
* Refusals (typed `SwapError`, never a silent fallback):
|
|
28
28
|
* - the pool is migrated or its curve is complete (`POOL_GRADUATED`): the template trades through
|
|
29
29
|
* Jupiter in that case;
|
|
30
|
+
* - the pool's config is not in the caller's `allowedConfigs` (`POOL_NOT_ALLOWED`): a launchpad
|
|
31
|
+
* site only trades its own coins;
|
|
30
32
|
* - the platform fee wallet does not exist or is not rent-exempt
|
|
31
33
|
* (`PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT`): the whole swap would fail on-chain (devnet finding);
|
|
32
|
-
* - the
|
|
34
|
+
* - the platform fee wallet is not exactly `FORGE_PLATFORM_FEE_WALLET`, or not a system account
|
|
35
|
+
* (`PLATFORM_FEE_WALLET_MISMATCH`);
|
|
36
|
+
* - the referral account is not an initialized wrapped SOL token account, or is not held by
|
|
37
|
+
* `FORGE_MULTISIG_VAULT` when the environment has it (`REFERRAL_ACCOUNT_INVALID`);
|
|
33
38
|
* - the pool is not a plain SOL-quoted DBC pool (`UNSUPPORTED_POOL`; transfer-hook pools excluded);
|
|
34
39
|
* - the serialized transaction exceeds 1,232 bytes (`TRANSACTION_TOO_LARGE`).
|
|
35
40
|
*
|
|
@@ -48,15 +53,15 @@ import { ComputeBudgetProgram, PACKET_DATA_SIZE, PublicKey, SYSVAR_INSTRUCTIONS_
|
|
|
48
53
|
import { ACCOUNT_SIZE, ExtensionType, NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, createSyncNativeInstruction, getAccountLen, getAssociatedTokenAddressSync, unpackAccount, } from '@solana/spl-token';
|
|
49
54
|
import BN from 'bn.js';
|
|
50
55
|
import { ActivationType, BaseFeeMode, DYNAMIC_BONDING_CURVE_PROGRAM_ID, DynamicBondingCurveIdl, TradeDirection, createDbcProgram, deriveDbcEventAuthority, deriveDbcPoolAuthority, getFeeMode, getTokenProgram, isRateLimiterApplied, swapQuote, } from '@meteora-ag/dynamic-bonding-curve-sdk';
|
|
51
|
-
import { requireAddresses } from './addresses.js';
|
|
56
|
+
import { readOptionalAddress, requireAddresses } from './addresses.js';
|
|
52
57
|
import { FEES } from './constants.js';
|
|
53
58
|
// ---------------------------------------------------------------------------------------------
|
|
54
59
|
// Constants
|
|
55
60
|
// ---------------------------------------------------------------------------------------------
|
|
56
61
|
/** Platform fee on swaps built by @forge/core, in bps (`FEES.platformFeeBps` from @forge/shared). */
|
|
57
62
|
export const PLATFORM_FEE_BPS = FEES.platformFeeBps;
|
|
58
|
-
/** Highest slippage tolerance accepted by the builders (
|
|
59
|
-
export const MAX_SWAP_SLIPPAGE_BPS =
|
|
63
|
+
/** Highest slippage tolerance accepted by the builders (25%), whether given in bps or as a minimum. */
|
|
64
|
+
export const MAX_SWAP_SLIPPAGE_BPS = 2_500;
|
|
60
65
|
/**
|
|
61
66
|
* Default compute unit limit. Measured on devnet: ~51,000 CU for a buy (~66,500 when it also creates
|
|
62
67
|
* the coin ATA), ~49,000 CU for a sell; the default leaves about 2x headroom (fee scheduler,
|
|
@@ -83,7 +88,11 @@ export const SWAP_ERROR_CODES = [
|
|
|
83
88
|
'POOL_NOT_FOUND',
|
|
84
89
|
'UNSUPPORTED_POOL',
|
|
85
90
|
'POOL_GRADUATED',
|
|
91
|
+
/** The pool's config is not in the caller's `allowedConfigs` (e.g. another launchpad's coin). */
|
|
92
|
+
'POOL_NOT_ALLOWED',
|
|
86
93
|
'PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT',
|
|
94
|
+
/** The platform fee wallet is not FORGE_PLATFORM_FEE_WALLET, or not a plain system account. */
|
|
95
|
+
'PLATFORM_FEE_WALLET_MISMATCH',
|
|
87
96
|
'REFERRAL_ACCOUNT_INVALID',
|
|
88
97
|
'INSUFFICIENT_LIQUIDITY',
|
|
89
98
|
'QUOTE_FAILED',
|
|
@@ -419,8 +428,19 @@ function coinAtaSize(tokenProgram) {
|
|
|
419
428
|
? getAccountLen([ExtensionType.ImmutableOwner])
|
|
420
429
|
: ACCOUNT_SIZE;
|
|
421
430
|
}
|
|
422
|
-
/**
|
|
423
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Throws unless the platform fee wallet exists and holds at least its rent-exempt minimum. With
|
|
433
|
+
* `expectedWallet` (FORGE_PLATFORM_FEE_WALLET, as `buildBuyTx` / `buildSellTx` pass it), the
|
|
434
|
+
* wallet must be exactly that key and a plain system account: lamports sent to an account owned by
|
|
435
|
+
* another program could not be spent by FORGE (`PLATFORM_FEE_WALLET_MISMATCH`).
|
|
436
|
+
*/
|
|
437
|
+
export function assertPlatformFeeWalletReady(wallet, account, rentExemptMinimum, expectedWallet) {
|
|
438
|
+
if (expectedWallet && !wallet.equals(expectedWallet)) {
|
|
439
|
+
throw new SwapError('PLATFORM_FEE_WALLET_MISMATCH', 'The platform fee must go to FORGE_PLATFORM_FEE_WALLET and nowhere else', { details: { platformFeeWallet: wallet.toBase58() } });
|
|
440
|
+
}
|
|
441
|
+
if (expectedWallet && account && !account.owner.equals(SystemProgram.programId)) {
|
|
442
|
+
throw new SwapError('PLATFORM_FEE_WALLET_MISMATCH', 'FORGE_PLATFORM_FEE_WALLET must be a system account (a wallet)', { details: { platformFeeWallet: wallet.toBase58(), owner: account.owner.toBase58() } });
|
|
443
|
+
}
|
|
424
444
|
const lamports = account?.lamports ?? 0n;
|
|
425
445
|
if (!account || lamports < rentExemptMinimum) {
|
|
426
446
|
throw new SwapError('PLATFORM_FEE_WALLET_NOT_RENT_EXEMPT', 'FORGE_PLATFORM_FEE_WALLET must exist and be rent-exempt, otherwise every swap fails on-chain', {
|
|
@@ -432,8 +452,12 @@ export function assertPlatformFeeWalletReady(wallet, account, rentExemptMinimum)
|
|
|
432
452
|
});
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
|
-
/**
|
|
436
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Throws unless the referral account is an initialized wrapped SOL token account (SPL Token).
|
|
457
|
+
* With `expectedOwner` (FORGE_MULTISIG_VAULT, when the environment has it), the token account must
|
|
458
|
+
* be held by that key: otherwise the referral share would be paid to someone else.
|
|
459
|
+
*/
|
|
460
|
+
export function assertReferralAccountReady(address, account, expectedOwner) {
|
|
437
461
|
const fail = (reason, cause) => new SwapError('REFERRAL_ACCOUNT_INVALID', `FORGE_METEORA_REFERRAL_ACCOUNT ${reason}`, {
|
|
438
462
|
details: { referralTokenAccount: address.toBase58() },
|
|
439
463
|
cause,
|
|
@@ -453,6 +477,27 @@ export function assertReferralAccountReady(address, account) {
|
|
|
453
477
|
throw fail('is not a wrapped SOL account');
|
|
454
478
|
if (!token.isInitialized || token.isFrozen)
|
|
455
479
|
throw fail('is not usable (uninitialized or frozen)');
|
|
480
|
+
if (expectedOwner && !token.owner.equals(expectedOwner)) {
|
|
481
|
+
throw fail('is not held by FORGE_MULTISIG_VAULT');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function toConfigKeys(allowedConfigs) {
|
|
485
|
+
if (!Array.isArray(allowedConfigs))
|
|
486
|
+
throw invalid('allowedConfigs must be an array');
|
|
487
|
+
return allowedConfigs.map((config, i) => toPublicKey(`allowedConfigs[${i}]`, config));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Throws `POOL_NOT_ALLOWED` unless the pool's config is one of `allowedConfigs`. Without a list
|
|
491
|
+
* (`undefined`), any config is accepted; an empty list accepts none.
|
|
492
|
+
*/
|
|
493
|
+
export function assertPoolConfigAllowed(pool, config, allowedConfigs) {
|
|
494
|
+
if (allowedConfigs === undefined)
|
|
495
|
+
return;
|
|
496
|
+
if (!toConfigKeys(allowedConfigs).some((allowed) => allowed.equals(config))) {
|
|
497
|
+
throw new SwapError('POOL_NOT_ALLOWED', 'This pool does not belong to this launchpad', {
|
|
498
|
+
details: { pool: pool.toBase58(), config: config.toBase58() },
|
|
499
|
+
});
|
|
500
|
+
}
|
|
456
501
|
}
|
|
457
502
|
function tokenBalance(address, account, tokenProgram) {
|
|
458
503
|
if (!account || !account.owner.equals(tokenProgram))
|
|
@@ -523,13 +568,14 @@ export function compileSwapTransaction(args) {
|
|
|
523
568
|
return { transaction, sizeBytes };
|
|
524
569
|
}
|
|
525
570
|
/**
|
|
526
|
-
* Pure transaction builder: validates the context (pool on its curve, fee accounts ready,
|
|
527
|
-
* quotes, and assembles the instructions in the order documented at
|
|
571
|
+
* Pure transaction builder: validates the context (pool on its curve, fee accounts ready and, with
|
|
572
|
+
* `expected`, exactly FORGE's), quotes, and assembles the instructions in the order documented at
|
|
573
|
+
* the top of this file.
|
|
528
574
|
*/
|
|
529
|
-
export function buildSwapTransaction(ctx, request) {
|
|
575
|
+
export function buildSwapTransaction(ctx, request, expected = {}) {
|
|
530
576
|
const options = readBuildOptions(request);
|
|
531
|
-
assertPlatformFeeWalletReady(ctx.platformFeeWallet, ctx.platformFeeWalletAccount, ctx.platformFeeWalletRentExemptMinimum);
|
|
532
|
-
assertReferralAccountReady(ctx.referralTokenAccount, ctx.referralTokenAccountInfo);
|
|
577
|
+
assertPlatformFeeWalletReady(ctx.platformFeeWallet, ctx.platformFeeWalletAccount, ctx.platformFeeWalletRentExemptMinimum, expected.platformFeeWallet);
|
|
578
|
+
assertReferralAccountReady(ctx.referralTokenAccount, ctx.referralTokenAccountInfo, expected.referralOwner);
|
|
533
579
|
const quote = computeSwapQuote(ctx, request);
|
|
534
580
|
const side = request.side;
|
|
535
581
|
const coinProgram = coinTokenProgram(ctx.virtualPool);
|
|
@@ -707,13 +753,20 @@ async function getCurrentPointRobust(connection, activationType, commitment) {
|
|
|
707
753
|
// Only used for the quote (fee scheduler); the program uses its own clock.
|
|
708
754
|
return new BN(blockTime ?? Math.floor(Date.now() / 1000));
|
|
709
755
|
}
|
|
710
|
-
/**
|
|
711
|
-
|
|
756
|
+
/**
|
|
757
|
+
* Reads and decodes a pool and its config, plus the current point. With
|
|
758
|
+
* `options.allowedConfigs`, a pool whose config is not in the list is refused (`POOL_NOT_ALLOWED`)
|
|
759
|
+
* before its config is read.
|
|
760
|
+
*/
|
|
761
|
+
export async function fetchSwapPoolState(connection, poolAddress, commitment = DEFAULT_COMMITMENT, options = {}) {
|
|
712
762
|
const pool = toPublicKey('pool', poolAddress);
|
|
763
|
+
if (options.allowedConfigs !== undefined)
|
|
764
|
+
toConfigKeys(options.allowedConfigs);
|
|
713
765
|
const program = dbcProgram(connection);
|
|
714
766
|
const poolInfo = await rpc('getAccountInfo(pool)', () => connection.getAccountInfo(pool, commitment));
|
|
715
767
|
const virtualPool = decodeDbcAccount(program, 'virtualPool', pool, snapshot(poolInfo));
|
|
716
768
|
const configAddress = virtualPool.poolState.config;
|
|
769
|
+
assertPoolConfigAllowed(pool, configAddress, options.allowedConfigs);
|
|
717
770
|
const configInfo = await rpc('getAccountInfo(config)', () => connection.getAccountInfo(configAddress, commitment));
|
|
718
771
|
const config = decodeDbcAccount(program, 'poolConfig', configAddress, snapshot(configInfo));
|
|
719
772
|
const currentPoint = await getCurrentPointRobust(connection, config.activationType, commitment);
|
|
@@ -725,6 +778,8 @@ export async function fetchSwapContext(p) {
|
|
|
725
778
|
const commitment = p.commitment ?? DEFAULT_COMMITMENT;
|
|
726
779
|
const user = toPublicKey('user', p.user);
|
|
727
780
|
const pool = toPublicKey('pool', p.pool);
|
|
781
|
+
if (p.allowedConfigs !== undefined)
|
|
782
|
+
toConfigKeys(p.allowedConfigs);
|
|
728
783
|
const program = dbcProgram(connection);
|
|
729
784
|
const [first, latest, slot] = await Promise.all([
|
|
730
785
|
rpc('getMultipleAccountsInfo', () => connection.getMultipleAccountsInfo([pool, p.platformFeeWallet, p.referralTokenAccount, user], commitment)),
|
|
@@ -734,6 +789,7 @@ export async function fetchSwapContext(p) {
|
|
|
734
789
|
const [poolInfo, platformInfo, referralInfo, userInfo] = first.map(snapshot);
|
|
735
790
|
const virtualPool = decodeDbcAccount(program, 'virtualPool', pool, poolInfo ?? null);
|
|
736
791
|
const configAddress = virtualPool.poolState.config;
|
|
792
|
+
assertPoolConfigAllowed(pool, configAddress, p.allowedConfigs);
|
|
737
793
|
const coinProgram = coinTokenProgram(virtualPool);
|
|
738
794
|
const userCoinAta = getAssociatedTokenAddressSync(virtualPool.poolState.baseMint, user, true, coinProgram);
|
|
739
795
|
const userWrappedSolAta = getAssociatedTokenAddressSync(NATIVE_MINT, user, true, TOKEN_PROGRAM_ID);
|
|
@@ -775,12 +831,17 @@ export async function fetchSwapContext(p) {
|
|
|
775
831
|
lastValidBlockHeight: latest.lastValidBlockHeight,
|
|
776
832
|
};
|
|
777
833
|
}
|
|
778
|
-
/**
|
|
834
|
+
/**
|
|
835
|
+
* FORGE fee accounts used by the swap, resolved through addresses.ts (never hard-coded), and the
|
|
836
|
+
* multisig vault that must hold the referral account (`null` when the environment does not have
|
|
837
|
+
* `FORGE_MULTISIG_VAULT`: the owner check is then skipped).
|
|
838
|
+
*/
|
|
779
839
|
export function swapFeeAddresses(env) {
|
|
780
840
|
const a = requireAddresses(['FORGE_PLATFORM_FEE_WALLET', 'FORGE_METEORA_REFERRAL_ACCOUNT'], env);
|
|
781
841
|
return {
|
|
782
842
|
platformFeeWallet: a.FORGE_PLATFORM_FEE_WALLET,
|
|
783
843
|
referralTokenAccount: a.FORGE_METEORA_REFERRAL_ACCOUNT,
|
|
844
|
+
multisigVault: readOptionalAddress('FORGE_MULTISIG_VAULT', env),
|
|
784
845
|
};
|
|
785
846
|
}
|
|
786
847
|
function slippageOf(p) {
|
|
@@ -791,7 +852,9 @@ async function buildSide(side, amountIn, p) {
|
|
|
791
852
|
assertU64(side === 'buy' ? 'amountInLamports' : 'amountInTokens', amountIn, 1n);
|
|
792
853
|
const slippage = readSlippage(slippageOf(p));
|
|
793
854
|
const options = readBuildOptions(p);
|
|
794
|
-
const { platformFeeWallet, referralTokenAccount } = swapFeeAddresses(p.env);
|
|
855
|
+
const { platformFeeWallet, referralTokenAccount, multisigVault } = swapFeeAddresses(p.env);
|
|
856
|
+
if (p.allowedConfigs !== undefined)
|
|
857
|
+
toConfigKeys(p.allowedConfigs);
|
|
795
858
|
const ctx = await fetchSwapContext({
|
|
796
859
|
connection: p.connection,
|
|
797
860
|
pool: p.pool,
|
|
@@ -799,8 +862,9 @@ async function buildSide(side, amountIn, p) {
|
|
|
799
862
|
platformFeeWallet,
|
|
800
863
|
referralTokenAccount,
|
|
801
864
|
...(p.commitment ? { commitment: p.commitment } : {}),
|
|
865
|
+
...(p.allowedConfigs !== undefined ? { allowedConfigs: p.allowedConfigs } : {}),
|
|
802
866
|
});
|
|
803
|
-
return buildSwapTransaction(ctx, { side, amountIn, slippage, ...options });
|
|
867
|
+
return buildSwapTransaction(ctx, { side, amountIn, slippage, ...options }, { platformFeeWallet, referralOwner: multisigVault });
|
|
804
868
|
}
|
|
805
869
|
/** Unsigned buy transaction (SOL -> coin) with the FORGE referral and the platform fee. */
|
|
806
870
|
export async function buildBuyTx(params) {
|
|
@@ -819,18 +883,19 @@ export async function quoteSwap(params) {
|
|
|
819
883
|
const slippage = params.slippageBps === undefined && params.minAmountOut === undefined
|
|
820
884
|
? { slippageBps: 0 }
|
|
821
885
|
: readSlippage(slippageOf(params));
|
|
822
|
-
const state = await fetchSwapPoolState(params.connection, params.pool, params.commitment);
|
|
886
|
+
const state = await fetchSwapPoolState(params.connection, params.pool, params.commitment, params.allowedConfigs !== undefined ? { allowedConfigs: params.allowedConfigs } : {});
|
|
823
887
|
return computeSwapQuote(state, { side: params.side, amountIn: params.amountIn, slippage });
|
|
824
888
|
}
|
|
825
889
|
/**
|
|
826
|
-
* Startup / pre-build check of the FORGE fee accounts: the platform fee wallet must be
|
|
827
|
-
* and the referral account must be a wrapped SOL token account
|
|
890
|
+
* Startup / pre-build check of the FORGE fee accounts: the platform fee wallet must be a
|
|
891
|
+
* rent-exempt system account and the referral account must be a wrapped SOL token account, held by
|
|
892
|
+
* `FORGE_MULTISIG_VAULT` when the environment has it. Throws `SwapError`.
|
|
828
893
|
*/
|
|
829
894
|
export async function assertSwapFeeAccountsReady(params) {
|
|
830
895
|
const commitment = params.commitment ?? DEFAULT_COMMITMENT;
|
|
831
|
-
const { platformFeeWallet, referralTokenAccount } = swapFeeAddresses(params.env);
|
|
896
|
+
const { platformFeeWallet, referralTokenAccount, multisigVault } = swapFeeAddresses(params.env);
|
|
832
897
|
const [platformInfo, referralInfo] = (await rpc('getMultipleAccountsInfo', () => params.connection.getMultipleAccountsInfo([platformFeeWallet, referralTokenAccount], commitment))).map(snapshot);
|
|
833
898
|
const rentMinimum = BigInt(await rpc('getMinimumBalanceForRentExemption', () => params.connection.getMinimumBalanceForRentExemption(platformInfo?.data.length ?? 0, commitment)));
|
|
834
|
-
assertPlatformFeeWalletReady(platformFeeWallet, platformInfo ?? null, rentMinimum);
|
|
835
|
-
assertReferralAccountReady(referralTokenAccount, referralInfo ?? null);
|
|
899
|
+
assertPlatformFeeWalletReady(platformFeeWallet, platformInfo ?? null, rentMinimum, platformFeeWallet);
|
|
900
|
+
assertReferralAccountReady(referralTokenAccount, referralInfo ?? null, multisigVault);
|
|
836
901
|
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* pool creation fee 0 or 0.001..100 SOL, ...), using the SDK constants and helpers;
|
|
9
9
|
* - FORGE's stricter bounds (client trading fee 50..200 bps, pool creation fee 0 or 0.001..1 SOL,
|
|
10
10
|
* migration threshold exactly 10 SOL, SOL quote, immutable metadata, ...);
|
|
11
|
+
* - FORGE's fixed settings, for every field that moves value: no migration fee, no locked
|
|
12
|
+
* vesting, the token supply of `FORGE_TOKEN_SETTINGS` (fixed, nothing minted later), FORGE's
|
|
13
|
+
* curve and start price (they decide the leftover sent to the leftover receiver), timestamp
|
|
14
|
+
* activation, and the anti-sniper schedule exactly `ANTI_SNIPER_FEE_SCHEDULE` (or a flat fee
|
|
15
|
+
* when `spec.antiSniper` is off);
|
|
11
16
|
* - that the parameters match the spec they were built from;
|
|
12
17
|
* - finally, the SDK's own `validateConfigParameters`, as a catch-all for curve-level rules.
|
|
13
18
|
* Lamport amounts are compared as `bigint`.
|
|
@@ -15,7 +20,15 @@
|
|
|
15
20
|
import { type ConfigParameters } from '@meteora-ag/dynamic-bonding-curve-sdk';
|
|
16
21
|
import { PublicKey } from '@solana/web3.js';
|
|
17
22
|
import type { LaunchpadSpec } from '@forge/shared';
|
|
18
|
-
export type ViolationCode = 'INVALID_ADDRESS' | 'QUOTE_NOT_SOL' | 'FEE_CLAIMER_MISMATCH' | 'LEFTOVER_RECEIVER_MISMATCH' | 'FEE_MODE_NOT_ALLOWED' | 'FEE_OUT_OF_SDK_BOUNDS' | 'FEE_OUT_OF_CLIENT_BOUNDS' | 'FEE_MISMATCH' | 'FEE_SCHEDULER_INVALID' | 'FEE_SCHEDULER_TOO_LONG' | 'ANTI_SNIPER_MISMATCH' | 'DYNAMIC_FEE_NOT_ALLOWED' | 'COLLECT_FEE_MODE_NOT_ALLOWED' | 'MIGRATION_OPTION_NOT_ALLOWED' | 'MIGRATION_FEE_OPTION_INVALID' | 'MIGRATION_FEE_INVALID'
|
|
23
|
+
export type ViolationCode = 'INVALID_ADDRESS' | 'QUOTE_NOT_SOL' | 'FEE_CLAIMER_MISMATCH' | 'LEFTOVER_RECEIVER_MISMATCH' | 'FEE_MODE_NOT_ALLOWED' | 'FEE_OUT_OF_SDK_BOUNDS' | 'FEE_OUT_OF_CLIENT_BOUNDS' | 'FEE_MISMATCH' | 'FEE_SCHEDULER_INVALID' | 'FEE_SCHEDULER_TOO_LONG' | 'ANTI_SNIPER_MISMATCH' | 'DYNAMIC_FEE_NOT_ALLOWED' | 'COLLECT_FEE_MODE_NOT_ALLOWED' | 'MIGRATION_OPTION_NOT_ALLOWED' | 'MIGRATION_FEE_OPTION_INVALID' | 'MIGRATION_FEE_INVALID'
|
|
24
|
+
/** A migration fee (to the partner or the creator) is set: FORGE configs take none. */
|
|
25
|
+
| 'MIGRATION_FEE_NOT_ALLOWED' | 'MIGRATED_POOL_FEE_OUT_OF_BOUNDS' | 'MIGRATED_POOL_FEE_INVALID' | 'MIGRATION_THRESHOLD_MISMATCH' | 'POOL_CREATION_FEE_OUT_OF_SDK_BOUNDS' | 'POOL_CREATION_FEE_OUT_OF_CLIENT_BOUNDS' | 'POOL_CREATION_FEE_MISMATCH' | 'CREATOR_TRADING_FEE_OUT_OF_BOUNDS' | 'CREATOR_TRADING_FEE_MISMATCH' | 'LP_PERCENTAGES_INVALID' | 'LIQUIDITY_VESTING_INVALID' | 'LOCKED_LIQUIDITY_TOO_LOW' | 'CREATOR_LOCKED_LIQUIDITY_REQUIRED' | 'TOKEN_UPDATE_AUTHORITY_NOT_IMMUTABLE'
|
|
26
|
+
/** Locked vesting of base tokens is set: FORGE configs vest nothing. */
|
|
27
|
+
| 'LOCKED_VESTING_NOT_ALLOWED'
|
|
28
|
+
/** Token supply differs from `FORGE_TOKEN_SETTINGS` (1,000,000,000 tokens, 6 decimals, fixed). */
|
|
29
|
+
| 'TOKEN_SUPPLY_MISMATCH'
|
|
30
|
+
/** Start price or curve differs from FORGE's (they decide the swap / migration / leftover split). */
|
|
31
|
+
| 'CURVE_MISMATCH' | 'TOKEN_TYPE_INVALID' | 'TOKEN_DECIMAL_INVALID' | 'ACTIVATION_TYPE_INVALID' | 'FIRST_SWAP_MIN_FEE_REQUIRED'
|
|
19
32
|
/** Coin name + symbol + metadata URI exceed the launch transaction budget. */
|
|
20
33
|
| 'LAUNCH_METADATA_TOO_LONG' | 'SPEC_INVALID' | 'SDK_REJECTED';
|
|
21
34
|
export interface Violation {
|
package/dist/validate.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* pool creation fee 0 or 0.001..100 SOL, ...), using the SDK constants and helpers;
|
|
9
9
|
* - FORGE's stricter bounds (client trading fee 50..200 bps, pool creation fee 0 or 0.001..1 SOL,
|
|
10
10
|
* migration threshold exactly 10 SOL, SOL quote, immutable metadata, ...);
|
|
11
|
+
* - FORGE's fixed settings, for every field that moves value: no migration fee, no locked
|
|
12
|
+
* vesting, the token supply of `FORGE_TOKEN_SETTINGS` (fixed, nothing minted later), FORGE's
|
|
13
|
+
* curve and start price (they decide the leftover sent to the leftover receiver), timestamp
|
|
14
|
+
* activation, and the anti-sniper schedule exactly `ANTI_SNIPER_FEE_SCHEDULE` (or a flat fee
|
|
15
|
+
* when `spec.antiSniper` is off);
|
|
11
16
|
* - that the parameters match the spec they were built from;
|
|
12
17
|
* - finally, the SDK's own `validateConfigParameters`, as a catch-all for curve-level rules.
|
|
13
18
|
* Lamport amounts are compared as `bigint`.
|
|
@@ -16,6 +21,7 @@ import { ActivationType, BaseFeeMode, CollectFeeMode, MigrationFeeOption, Migrat
|
|
|
16
21
|
import { PublicKey } from '@solana/web3.js';
|
|
17
22
|
import BN from 'bn.js';
|
|
18
23
|
import { CLIENT_BOUNDS, CLIENT_POOL_CREATION_FEE_LAMPORTS, FEE_DENOMINATOR, FEES, MAX_ANTI_SNIPER_DURATION_SECONDS, MAX_ANTI_SNIPER_DURATION_SLOTS, MAX_ANTI_SNIPER_STARTING_FEE_BPS, MAX_BASIS_POINT, MAX_FEE_BPS, MAX_FEE_NUMERATOR, MAX_FIRST_BUY_SOL, MAX_MIGRATED_POOL_FEE_BPS, MAX_POOL_CREATION_FEE, MIGRATION_THRESHOLD_LAMPORTS, MIN_FEE_BPS, MIN_FEE_NUMERATOR, MIN_LOCKED_LIQUIDITY_BPS, MIN_MIGRATED_POOL_FEE_BPS, MIN_POOL_CREATION_FEE, SECONDS_PER_DAY, SOL_QUOTE_MINT, solToLamports, } from './constants.js';
|
|
24
|
+
import { ANTI_SNIPER_FEE_SCHEDULE, FORGE_ACTIVATION_TYPE, FORGE_TOKEN_SETTINGS, FORGE_TOKEN_SUPPLY_RAW, forgeBaseFee, forgeReferenceCurve, } from './settings.js';
|
|
19
25
|
/** Thrown by `assertValid` when a result has violations. */
|
|
20
26
|
export class ValidationError extends Error {
|
|
21
27
|
violations;
|
|
@@ -95,6 +101,15 @@ const FEE_NUMERATOR_PER_BPS = BigInt(FEE_DENOMINATOR / MAX_BASIS_POINT);
|
|
|
95
101
|
function bpsToNumerator(bps) {
|
|
96
102
|
return BigInt(bpsToFeeNumerator(bps).toString());
|
|
97
103
|
}
|
|
104
|
+
/** FORGE's base fee for this fee and anti-sniper flag, or null when the SDK refuses the fee. */
|
|
105
|
+
function expectedBaseFee(tradingFeeBps, antiSniper) {
|
|
106
|
+
try {
|
|
107
|
+
return forgeBaseFee(tradingFeeBps, antiSniper);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
98
113
|
/** Fee numerator to bps, rounded to the nearest bps (for messages and the bounds check). */
|
|
99
114
|
function numeratorToRoundedBps(numerator) {
|
|
100
115
|
return Number((numerator + FEE_NUMERATOR_PER_BPS / 2n) / FEE_NUMERATOR_PER_BPS);
|
|
@@ -212,6 +227,7 @@ function validateConfig(config, spec, expected) {
|
|
|
212
227
|
collectTradingFee(config, spec, c);
|
|
213
228
|
collectModes(config, c);
|
|
214
229
|
collectMigration(config, c);
|
|
230
|
+
collectSupply(config, c);
|
|
215
231
|
collectPoolCreationFee(config, spec, expected, c);
|
|
216
232
|
collectCreatorTradingFee(config, expected, c);
|
|
217
233
|
collectLiquidity(config, expected, c);
|
|
@@ -343,6 +359,26 @@ function collectTradingFee(config, spec, c) {
|
|
|
343
359
|
else if (spec?.antiSniper === false && !schedulerOff) {
|
|
344
360
|
c.add(field, 'ANTI_SNIPER_MISMATCH', 'spec.antiSniper is off: the fee scheduler must be disabled');
|
|
345
361
|
}
|
|
362
|
+
// FORGE's schedule exactly, not just one within the bounds: a single long period at 99% (or an
|
|
363
|
+
// exponential curve) would keep traders paying the starting fee far longer than announced.
|
|
364
|
+
const reported = c.violations.some((v) => v.field === field && (v.code === 'ANTI_SNIPER_MISMATCH' || v.code === 'FEE_MISMATCH'));
|
|
365
|
+
if (!reported &&
|
|
366
|
+
typeof spec?.antiSniper === 'boolean' &&
|
|
367
|
+
isIntInRange(spec.tradingFeeBps, MIN_FEE_BPS, MAX_FEE_BPS)) {
|
|
368
|
+
const forge = expectedBaseFee(spec.tradingFeeBps, spec.antiSniper);
|
|
369
|
+
const same = forge !== null &&
|
|
370
|
+
toBigInt(forge.cliffFeeNumerator) === cliff &&
|
|
371
|
+
forge.firstFactor === numberOfPeriod &&
|
|
372
|
+
toBigInt(forge.secondFactor) === periodFrequency &&
|
|
373
|
+
toBigInt(forge.thirdFactor) === reductionFactor &&
|
|
374
|
+
forge.baseFeeMode === mode;
|
|
375
|
+
if (!same) {
|
|
376
|
+
const schedule = ANTI_SNIPER_FEE_SCHEDULE;
|
|
377
|
+
c.add(field, spec.antiSniper ? 'ANTI_SNIPER_MISMATCH' : 'FEE_MISMATCH', spec.antiSniper
|
|
378
|
+
? `the anti-sniper schedule must be exactly ANTI_SNIPER_FEE_SCHEDULE: linear from ${schedule.startingFeeBps} bps to spec.tradingFeeBps in ${schedule.numberOfPeriod} periods over ${schedule.totalDurationSeconds} s`
|
|
379
|
+
: 'spec.antiSniper is off: the base fee must be a flat spec.tradingFeeBps in FeeSchedulerLinear mode');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
346
382
|
if (config.poolFees.dynamicFee !== null && config.poolFees.dynamicFee !== undefined) {
|
|
347
383
|
c.add('poolFees.dynamicFee', 'DYNAMIC_FEE_NOT_ALLOWED', 'dynamic fee must be disabled (it would raise the fee above the client bounds)');
|
|
348
384
|
}
|
|
@@ -351,14 +387,53 @@ function collectModes(config, c) {
|
|
|
351
387
|
if (config.collectFeeMode !== CollectFeeMode.QuoteToken) {
|
|
352
388
|
c.add('collectFeeMode', 'COLLECT_FEE_MODE_NOT_ALLOWED', 'collectFeeMode must be QuoteToken (fees and the referral are paid in SOL)');
|
|
353
389
|
}
|
|
354
|
-
|
|
355
|
-
|
|
390
|
+
// The anti-sniper periods are counted in seconds: a slot-based config would run them in slots.
|
|
391
|
+
if (!enumValues(ActivationType).includes(config.activationType) ||
|
|
392
|
+
config.activationType !== FORGE_ACTIVATION_TYPE) {
|
|
393
|
+
c.add('activationType', 'ACTIVATION_TYPE_INVALID', 'activationType must be Timestamp (FORGE_ACTIVATION_TYPE)');
|
|
394
|
+
}
|
|
395
|
+
if (!enumValues(TokenType).includes(config.tokenType) ||
|
|
396
|
+
config.tokenType !== FORGE_TOKEN_SETTINGS.tokenType) {
|
|
397
|
+
c.add('tokenType', 'TOKEN_TYPE_INVALID', 'tokenType must be SPLToken (FORGE_TOKEN_SETTINGS)');
|
|
356
398
|
}
|
|
357
|
-
if (!enumValues(
|
|
358
|
-
|
|
399
|
+
if (!enumValues(TokenDecimal).includes(config.tokenDecimal) ||
|
|
400
|
+
config.tokenDecimal !== FORGE_TOKEN_SETTINGS.tokenBaseDecimal) {
|
|
401
|
+
c.add('tokenDecimal', 'TOKEN_DECIMAL_INVALID', `tokenDecimal must be ${FORGE_TOKEN_SETTINGS.tokenBaseDecimal} (FORGE_TOKEN_SETTINGS)`);
|
|
359
402
|
}
|
|
360
|
-
|
|
361
|
-
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Supply side (every field decides where tokens go): no locked vesting, FORGE's fixed token
|
|
406
|
+
* supply, and FORGE's curve and start price (with a fixed supply they decide how much is swapped,
|
|
407
|
+
* migrated and left over to the leftover receiver).
|
|
408
|
+
*/
|
|
409
|
+
function collectSupply(config, c) {
|
|
410
|
+
const vesting = config.lockedVesting;
|
|
411
|
+
const vestingFields = vesting
|
|
412
|
+
? [
|
|
413
|
+
vesting.amountPerPeriod,
|
|
414
|
+
vesting.cliffDurationFromMigrationTime,
|
|
415
|
+
vesting.frequency,
|
|
416
|
+
vesting.numberOfPeriod,
|
|
417
|
+
vesting.cliffUnlockAmount,
|
|
418
|
+
]
|
|
419
|
+
: [];
|
|
420
|
+
if (!vesting || vestingFields.some((value) => toBigInt(value) !== 0n)) {
|
|
421
|
+
c.add('lockedVesting', 'LOCKED_VESTING_NOT_ALLOWED', 'lockedVesting must be all zero (FORGE configs vest no base token)');
|
|
422
|
+
}
|
|
423
|
+
const supply = config.tokenSupply;
|
|
424
|
+
if (!supply ||
|
|
425
|
+
toBigInt(supply.preMigrationTokenSupply) !== FORGE_TOKEN_SUPPLY_RAW ||
|
|
426
|
+
toBigInt(supply.postMigrationTokenSupply) !== FORGE_TOKEN_SUPPLY_RAW) {
|
|
427
|
+
c.add('tokenSupply', 'TOKEN_SUPPLY_MISMATCH', `tokenSupply must be fixed at ${FORGE_TOKEN_SUPPLY_RAW} raw units before and after migration (FORGE_TOKEN_SETTINGS)`);
|
|
428
|
+
}
|
|
429
|
+
const reference = forgeReferenceCurve();
|
|
430
|
+
const curve = Array.isArray(config.curve) ? config.curve : [];
|
|
431
|
+
const sameCurve = toBigInt(config.sqrtStartPrice) === toBigInt(reference.sqrtStartPrice) &&
|
|
432
|
+
curve.length === reference.curve.length &&
|
|
433
|
+
reference.curve.every((point, i) => toBigInt(curve[i]?.sqrtPrice) === toBigInt(point.sqrtPrice) &&
|
|
434
|
+
toBigInt(curve[i]?.liquidity) === toBigInt(point.liquidity));
|
|
435
|
+
if (!sameCurve) {
|
|
436
|
+
c.add('curve', 'CURVE_MISMATCH', "sqrtStartPrice and curve must be FORGE's (FORGE_TOKEN_SETTINGS, FORGE_MIGRATION_SETTINGS, 10 SOL threshold)");
|
|
362
437
|
}
|
|
363
438
|
}
|
|
364
439
|
function collectMigration(config, c) {
|
|
@@ -379,6 +454,11 @@ function collectMigration(config, c) {
|
|
|
379
454
|
if (feeReason !== null) {
|
|
380
455
|
c.add('migrationFee', 'MIGRATION_FEE_INVALID', `migrationFee: ${feeReason}`);
|
|
381
456
|
}
|
|
457
|
+
else if (config.migrationFee.feePercentage !== 0 ||
|
|
458
|
+
config.migrationFee.creatorFeePercentage !== 0) {
|
|
459
|
+
// The SDK allows up to 99% / 100% of the migrated quote: on a FORGE config it is always 0.
|
|
460
|
+
c.add('migrationFee', 'MIGRATION_FEE_NOT_ALLOWED', 'migrationFee.feePercentage and migrationFee.creatorFeePercentage must be 0');
|
|
461
|
+
}
|
|
382
462
|
const pool = config.migratedPoolFee;
|
|
383
463
|
if (!pool) {
|
|
384
464
|
c.add('migratedPoolFee', 'MIGRATED_POOL_FEE_INVALID', 'migratedPoolFee is required');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgedevv/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "FORGE transaction core: Meteora Dynamic Bonding Curve configs, pools, swaps, claims and on-chain checks. Published on npm as @forgedevv/core.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"registry": "https://registry.npmjs.org/"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@forge/shared": "npm:@forgedevv/shared@0.
|
|
50
|
+
"@forge/shared": "npm:@forgedevv/shared@0.2.0",
|
|
51
|
+
"@meteora-ag/cp-amm-sdk": "1.5.1",
|
|
51
52
|
"@meteora-ag/dynamic-bonding-curve-sdk": "1.5.13",
|
|
52
53
|
"@solana/spl-token": "0.4.15",
|
|
53
54
|
"@solana/web3.js": "1.99.0",
|