@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 CHANGED
@@ -3,13 +3,96 @@
3
3
  All notable changes to `@forge/core`. Versions follow semantic versioning. Consumers (the
4
4
  launchpad template, the signer, the web app) pin an exact version, never a range.
5
5
 
6
- ## 0.1.0 — unreleased
7
-
8
- First version meant for publication on npm as `@forgedevv/core` (with `@forgedevv/shared`), through
9
- `node scripts/publish-core.mjs --publish`. Not published yet.
6
+ ## 0.2.0 — unreleased
7
+
8
+ Everything below was added after 0.1.0 was published. Publish `@forgedevv/shared` 0.2.0 with it.
9
+
10
+ ### Changed (security review)
11
+
12
+ - **On-chain configs compared field by field.** New `onchainConfigMismatches(config, kind,
13
+ { spec?, owner?, poolCreationFeeLamports? })` (config.ts) compares a decoded config with the
14
+ parameters core builds for it: accounts, token, base fee (cliff, periods, period, reduction,
15
+ mode), dynamic fee, creator share, pool creation fee, first swap at the minimum fee, activation,
16
+ liquidity split and vesting, migration option / fee option / threshold, migration fee
17
+ percentages, migrated pool fee, locked vesting, token supply, start price and curve. Without a
18
+ spec, the spec fields are read from the config and must be values a FORGE spec can hold.
19
+ - `assertLaunchCoinConfig(config, owner, spec?)`: runs it (optional `spec`, recommended).
20
+ `BuildLaunchCoinTxParams.spec?` passes it. A coin config with a pool creation fee is now
21
+ `CONFIG_MISMATCH` (the current builder sets none).
22
+ - `assertUserCoinConfig(config, expected?)`: runs it against the launchpad config;
23
+ `BuildUserCoinLaunchTxParams.expectedConfig?` passes the expectation.
24
+ - **validate.ts pins every value-moving field to FORGE's settings**: no migration fee
25
+ (`MIGRATION_FEE_NOT_ALLOWED`), no locked vesting (`LOCKED_VESTING_NOT_ALLOWED`), the fixed supply
26
+ of `FORGE_TOKEN_SETTINGS` (`TOKEN_SUPPLY_MISMATCH`), FORGE's curve and start price
27
+ (`CURVE_MISMATCH`, they decide the leftover), SPL token with 6 decimals, timestamp activation,
28
+ and the anti-sniper schedule exactly `ANTI_SNIPER_FEE_SCHEDULE` (a flat linear fee when
29
+ `spec.antiSniper` is off): an exponential schedule, another starting fee or one 300 s period at
30
+ 99% are refused.
31
+ - The fixed settings moved to `settings.ts` (re-exported by config.ts, unchanged values), with
32
+ `FORGE_TOKEN_SUPPLY_RAW`, `forgeFeeSchedulerParam`, `forgeBaseFee`, `forgeReferenceCurve`.
33
+ `curveParamsFor`, `creatorTradingFeePercentageFor` and `poolCreationFeeSolFor` accept just the
34
+ spec fields they read (`ConfigSpecFields`); a full `LaunchpadSpec` still fits.
35
+ - **Platform fee on a user coin's first buy.** `buildUserCoinLaunchTx` adds ONE SystemProgram
36
+ transfer of `platformFeeLamports(firstBuyLamports)` (30 bps, rounded up) to
37
+ `FORGE_PLATFORM_FEE_WALLET` (read from `env`), as the last instruction; `chargePlatformFee:
38
+ false` turns it off. The wallet must be rent-exempt (`PLATFORM_FEE_WALLET_NOT_READY`). The
39
+ summary gains `platformFeeLamports` and `platformFeeWallet`; `LaunchCoinCosts.platformFeeLamports`
40
+ (optional) is set and counted in `ownerTotalLamports`.
41
+ - **Swaps.** `MAX_SWAP_SLIPPAGE_BPS` is 2,500 (25%, was 50%). `buildBuyTx`, `buildSellTx`,
42
+ `quoteSwap`, `fetchSwapContext` and `fetchSwapPoolState` take an optional `allowedConfigs`: a
43
+ pool whose on-chain config is not listed is refused (`POOL_NOT_ALLOWED`) before its config is
44
+ read. The builders now require the platform fee wallet to be exactly `FORGE_PLATFORM_FEE_WALLET`
45
+ and a system account (`PLATFORM_FEE_WALLET_MISMATCH`), and, when the environment has
46
+ `FORGE_MULTISIG_VAULT`, the Meteora referral token account to be held by it
47
+ (`REFERRAL_ACCOUNT_INVALID`). `buildSwapTransaction(ctx, request, expected?)`,
48
+ `assertPlatformFeeWalletReady(..., expectedWallet?)`, `assertReferralAccountReady(...,
49
+ expectedOwner?)`, new `assertPoolConfigAllowed`, `readOptionalAddress` (addresses.ts);
50
+ `swapFeeAddresses` also returns `multisigVault`.
51
+ - `checkPaymentTransaction` / `verifyPayment`: `transferLamports` on `amount_mismatch` when the transfer amount differs.
52
+ - Jupiter referral fee claim (`jupiter-referral.ts`, mainnet-beta only): `buildJupiterReferralClaimTx`
53
+ reads the referral account, its project and the two collection accounts of a mint (classic
54
+ `referral_ata` PDA and the referral account's associated token account) and builds the
55
+ permissionless `claim` / `claim_v2` instructions to the referral account's partner (the FORGE
56
+ multisig vault, asserted with `expectedPartner`). Raw instructions from the referral program IDL
57
+ (no Anchor dependency), with the pure pieces exported (`decodeJupiterReferralAccount`,
58
+ `decodeJupiterProject`, `deriveJupiterReferralTokenAccount[V2]`, `buildJupiterReferralClaimIx`,
59
+ `buildJupiterCreateAdminTokenAccountIx`). Any cluster but mainnet-beta is refused
60
+ (`JupiterReferralError` `wrong_cluster`). The summary also carries the project's `admin` and
61
+ `projectAuthority`, so a caller can pin every account of the built claim.
62
+ - Squads accounts of the buyback read in addresses.ts (PLANEXECUTE rule 7): `FORGE_SQUADS_MULTISIG`
63
+ and `FORGE_SQUADS_SPENDING_LIMIT` join `FORGE_ADDRESS_NAMES` as optional names;
64
+ `readSquadsAddresses(env)` returns them with `FORGE_SQUADS_VAULT_INDEX` (default 0) or `null`
65
+ when both are absent, and refuses half a configuration, an index without them, an index outside
66
+ 0..255 or a spending limit equal to the multisig (`ForgeSquadsConfigError`). Deriving the vault
67
+ needs the Squads program id: the signer keeps that check.
68
+
69
+ ### Added (DAMM v2 position fee claims)
70
+
71
+ - `position-claims.ts`: after a launchpad coin migrates, the FORGE creator wallet keeps a
72
+ permanently locked DAMM v2 position that earns the migrated pool's trading fees.
73
+ `buildCreatorPositionFeeClaimTx` builds the unsigned `claim_position_fee` of the creator's
74
+ position(s) to a receiver (`buildForgeCreatorPositionFeeClaimTx`: the multisig vault), creator
75
+ signing, optional fee payer: coin fees to the receiver's associated account, SOL fees to the
76
+ creator's temporary WSOL account closed to the receiver (fees plus rent as lamports). The
77
+ transaction never removes liquidity nor closes, locks or transfers the position or its NFT.
78
+ Summary, costs (`PositionClaimCosts`), skip reasons (`no_position`, `nothing_to_claim`,
79
+ `not_worthwhile`), `isPositionClaimWorthwhile`. Pure helpers: `migratedDammV2Pool` (the DAMM v2
80
+ pool of a migrated DBC pool, derived like the SDK migration), `positionPendingFees`,
81
+ `summarizePosition`, `sumPendingFees`; reads: `findCreatorDammV2Positions` (the creator's
82
+ positions, NFT ownership re-checked), `getDammV2PoolState`. Constants for policies:
83
+ `DAMM_V2_PROGRAM_ID`, `CLAIM_POSITION_FEE_DISCRIMINATOR`, `CLAIM_POSITION_FEE_ACCOUNTS`.
84
+ New `ClaimError` codes: `not_migrated`, `damm_pool_not_found`, `damm_pool_mismatch`,
85
+ `position_not_found`, `position_mismatch`, `position_not_owned`.
86
+ - Dependency: `@meteora-ag/cp-amm-sdk` 1.5.1 (exact pin, like the DBC SDK).
87
+ - Fixtures: `__fixtures__/cp-amm-devnet-accounts.json`, the devnet test 2 migration captured
88
+ read-only (`scripts/devnet-tests` `fixtures:cp-amm`).
89
+
90
+ ## 0.1.0 — 2026-10-14
91
+
92
+ First version published on npm as `@forgedevv/core` (with `@forgedevv/shared`), through
93
+ `node scripts/publish-core.mjs --publish`.
10
94
 
11
95
  - `listCoinsOfCreator` (creator pools through SDK `getPoolsByCreator`), `DBC_POOL_CREATOR_OFFSET`, `DBC_POOL_CONFIG_OFFSET`.
12
- - `checkPaymentTransaction` / `verifyPayment`: `transferLamports` on `amount_mismatch` when the transfer amount differs.
13
96
 
14
97
  ### Changed (wave 5 review)
15
98
 
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # @forgedevv/core
2
2
 
3
3
  The FORGE transaction core: it builds the Meteora Dynamic Bonding Curve configs, the launchpad coin
4
- launch, buys and sells with the FORGE platform fee and referral, creator fee claims, payment checks
5
- and the read helpers used by FORGE launchpad sites. Every function that builds a transaction
6
- returns it **unsigned**: the user signs in their wallet.
4
+ launch, buys and sells with the FORGE platform fee and referral, creator fee claims (bonding curve
5
+ share, and the DAMM v2 position fees of a migrated pool), payment checks and the read helpers used
6
+ by FORGE launchpad sites. Every function that builds a transaction returns it **unsigned**: the
7
+ user signs in their wallet.
7
8
 
8
9
  Inside the FORGE monorepo this package is `@forge/core`. It is published on the public npm registry
9
10
  as `@forgedevv/core` (with its contracts package `@forgedevv/shared`), and client projects install
@@ -16,10 +16,13 @@
16
16
  * Error messages list variable names only, never values.
17
17
  */
18
18
  import { PublicKey } from '@solana/web3.js';
19
- export declare const FORGE_ADDRESS_NAMES: readonly ["FORGE_MULTISIG_VAULT", "FORGE_METEORA_REFERRAL_ACCOUNT", "FORGE_PLATFORM_FEE_WALLET", "FORGE_JUPITER_REFERRAL_ACCOUNT", "FORGE_CASHBOX_WALLET", "FORGE_MINT"];
19
+ export declare const FORGE_ADDRESS_NAMES: readonly ["FORGE_MULTISIG_VAULT", "FORGE_METEORA_REFERRAL_ACCOUNT", "FORGE_PLATFORM_FEE_WALLET", "FORGE_JUPITER_REFERRAL_ACCOUNT", "FORGE_CASHBOX_WALLET", "FORGE_MINT", "FORGE_SQUADS_MULTISIG", "FORGE_SQUADS_SPENDING_LIMIT"];
20
20
  export type ForgeAddressName = (typeof FORGE_ADDRESS_NAMES)[number];
21
- /** Addresses that may be absent. `FORGE_MINT` is absent before the $FORGE launch. */
22
- export declare const OPTIONAL_FORGE_ADDRESS_NAMES: readonly ["FORGE_MINT"];
21
+ /**
22
+ * Addresses that may be absent. `FORGE_MINT` is absent before the $FORGE launch; the Squads
23
+ * accounts are absent while the buyback stays in simulation (see `readSquadsAddresses`).
24
+ */
25
+ export declare const OPTIONAL_FORGE_ADDRESS_NAMES: readonly ["FORGE_MINT", "FORGE_SQUADS_MULTISIG", "FORGE_SQUADS_SPENDING_LIMIT"];
23
26
  export type OptionalForgeAddressName = (typeof OPTIONAL_FORGE_ADDRESS_NAMES)[number];
24
27
  /** Environment object to read from (defaults to `process.env`; injectable for tests). */
25
28
  export type AddressEnv = Readonly<Record<string, string | undefined>>;
@@ -41,6 +44,15 @@ export declare class ForgeAddressError extends Error {
41
44
  * @throws ForgeAddressError listing the missing and invalid names (never the values).
42
45
  */
43
46
  export declare function requireAddresses<N extends ForgeAddressName>(names: readonly N[], env?: AddressEnv): ForgeAddresses<N>;
47
+ /**
48
+ * One FORGE address when it is set, `null` when it is absent or empty. A value that is set but is
49
+ * not a valid public key is refused, exactly as `requireAddresses` does. For checks that run
50
+ * wherever the address is known and are skipped elsewhere (e.g. the owner of the Meteora referral
51
+ * account, `FORGE_MULTISIG_VAULT`, in a browser that was not given it).
52
+ *
53
+ * @throws ForgeAddressError (invalid value; names only).
54
+ */
55
+ export declare function readOptionalAddress(name: ForgeAddressName, env?: AddressEnv): PublicKey | null;
44
56
  /** Squads multisig vault: receives the creator fees, the Meteora referral and the cashbox sweeps. */
45
57
  export declare function getMultisigVault(env?: AddressEnv): PublicKey;
46
58
  /** Meteora referral token account (WSOL, held by the multisig), passed as `referralTokenAccount`. */
@@ -53,3 +65,38 @@ export declare function getJupiterReferralAccount(env?: AddressEnv): PublicKey;
53
65
  export declare function getCashboxWallet(env?: AddressEnv): PublicKey;
54
66
  /** $FORGE mint, or `null` before the $FORGE launch (token-gating off, buyback simulated). */
55
67
  export declare function getForgeMint(env?: AddressEnv): PublicKey | null;
68
+ /** Squads v4 accounts the buyback withdraws through, read from the environment. */
69
+ export interface SquadsAddresses {
70
+ /** FORGE_SQUADS_MULTISIG: the multisig account. */
71
+ multisig: PublicKey;
72
+ /** FORGE_SQUADS_SPENDING_LIMIT: the spending limit account of the buyback wallet. */
73
+ spendingLimit: PublicKey;
74
+ /** FORGE_SQUADS_VAULT_INDEX (default 0): the vault index of the treasury vault. */
75
+ vaultIndex: number;
76
+ }
77
+ export type SquadsAddressProblem =
78
+ /** One of the two addresses without the other. */
79
+ 'incomplete'
80
+ /** FORGE_SQUADS_VAULT_INDEX other than the default 0 without the addresses. */
81
+ | 'index_without_multisig'
82
+ /** FORGE_SQUADS_VAULT_INDEX is not an integer between 0 and 255. */
83
+ | 'invalid_index'
84
+ /** The spending limit account is the multisig account. */
85
+ | 'not_distinct';
86
+ /** Thrown by `readSquadsAddresses` on a rule the names alone cannot express. Names only. */
87
+ export declare class ForgeSquadsConfigError extends Error {
88
+ readonly problem: SquadsAddressProblem;
89
+ constructor(problem: SquadsAddressProblem, message: string);
90
+ }
91
+ /**
92
+ * FORGE_SQUADS_MULTISIG, FORGE_SQUADS_SPENDING_LIMIT and FORGE_SQUADS_VAULT_INDEX. Returns `null`
93
+ * when both addresses are absent (no withdrawer: the buyback stays in simulation). Rules: the
94
+ * two addresses are set together or not at all; an index other than the default `0` needs them
95
+ * (a lone `0` is the default spelled out, not a configuration); the index is an integer between
96
+ * 0 and 255 (default 0); the spending limit is a distinct account. Deriving the vault from the
97
+ * multisig and the index, and comparing it with FORGE_MULTISIG_VAULT, needs the Squads program
98
+ * id (pinned SDK): the caller (the signer) does it.
99
+ *
100
+ * @throws ForgeAddressError (invalid address, names only), ForgeSquadsConfigError (other rules).
101
+ */
102
+ export declare function readSquadsAddresses(env?: AddressEnv): SquadsAddresses | null;
package/dist/addresses.js CHANGED
@@ -23,10 +23,17 @@ export const FORGE_ADDRESS_NAMES = [
23
23
  'FORGE_JUPITER_REFERRAL_ACCOUNT',
24
24
  'FORGE_CASHBOX_WALLET',
25
25
  'FORGE_MINT',
26
+ 'FORGE_SQUADS_MULTISIG',
27
+ 'FORGE_SQUADS_SPENDING_LIMIT',
26
28
  ];
27
- /** Addresses that may be absent. `FORGE_MINT` is absent before the $FORGE launch. */
29
+ /**
30
+ * Addresses that may be absent. `FORGE_MINT` is absent before the $FORGE launch; the Squads
31
+ * accounts are absent while the buyback stays in simulation (see `readSquadsAddresses`).
32
+ */
28
33
  export const OPTIONAL_FORGE_ADDRESS_NAMES = [
29
34
  'FORGE_MINT',
35
+ 'FORGE_SQUADS_MULTISIG',
36
+ 'FORGE_SQUADS_SPENDING_LIMIT',
30
37
  ];
31
38
  /** Thrown when required addresses are missing or invalid. Holds names only, never values. */
32
39
  export class ForgeAddressError extends Error {
@@ -101,6 +108,20 @@ export function requireAddresses(names, env = defaultEnv()) {
101
108
  throw new ForgeAddressError(missing, invalid);
102
109
  return result;
103
110
  }
111
+ /**
112
+ * One FORGE address when it is set, `null` when it is absent or empty. A value that is set but is
113
+ * not a valid public key is refused, exactly as `requireAddresses` does. For checks that run
114
+ * wherever the address is known and are skipped elsewhere (e.g. the owner of the Meteora referral
115
+ * account, `FORGE_MULTISIG_VAULT`, in a browser that was not given it).
116
+ *
117
+ * @throws ForgeAddressError (invalid value; names only).
118
+ */
119
+ export function readOptionalAddress(name, env = defaultEnv()) {
120
+ const value = env[name];
121
+ if (value === undefined || value === '')
122
+ return null;
123
+ return requireAddresses([name], env)[name];
124
+ }
104
125
  /** Squads multisig vault: receives the creator fees, the Meteora referral and the cashbox sweeps. */
105
126
  export function getMultisigVault(env) {
106
127
  return requireAddresses(['FORGE_MULTISIG_VAULT'], env).FORGE_MULTISIG_VAULT;
@@ -125,3 +146,51 @@ export function getCashboxWallet(env) {
125
146
  export function getForgeMint(env) {
126
147
  return requireAddresses(['FORGE_MINT'], env).FORGE_MINT;
127
148
  }
149
+ /** Thrown by `readSquadsAddresses` on a rule the names alone cannot express. Names only. */
150
+ export class ForgeSquadsConfigError extends Error {
151
+ problem;
152
+ constructor(problem, message) {
153
+ super(message);
154
+ this.name = 'ForgeSquadsConfigError';
155
+ this.problem = problem;
156
+ }
157
+ }
158
+ const VAULT_INDEX_RE = /^(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
159
+ /**
160
+ * FORGE_SQUADS_MULTISIG, FORGE_SQUADS_SPENDING_LIMIT and FORGE_SQUADS_VAULT_INDEX. Returns `null`
161
+ * when both addresses are absent (no withdrawer: the buyback stays in simulation). Rules: the
162
+ * two addresses are set together or not at all; an index other than the default `0` needs them
163
+ * (a lone `0` is the default spelled out, not a configuration); the index is an integer between
164
+ * 0 and 255 (default 0); the spending limit is a distinct account. Deriving the vault from the
165
+ * multisig and the index, and comparing it with FORGE_MULTISIG_VAULT, needs the Squads program
166
+ * id (pinned SDK): the caller (the signer) does it.
167
+ *
168
+ * @throws ForgeAddressError (invalid address, names only), ForgeSquadsConfigError (other rules).
169
+ */
170
+ export function readSquadsAddresses(env = defaultEnv()) {
171
+ const multisigRaw = env.FORGE_SQUADS_MULTISIG ?? '';
172
+ const limitRaw = env.FORGE_SQUADS_SPENDING_LIMIT ?? '';
173
+ const indexRaw = env.FORGE_SQUADS_VAULT_INDEX ?? '';
174
+ if (multisigRaw === '' && limitRaw === '') {
175
+ if (indexRaw !== '' && indexRaw !== '0') {
176
+ throw new ForgeSquadsConfigError('index_without_multisig', 'FORGE_SQUADS_VAULT_INDEX needs FORGE_SQUADS_MULTISIG');
177
+ }
178
+ return null;
179
+ }
180
+ if (multisigRaw === '' || limitRaw === '') {
181
+ throw new ForgeSquadsConfigError('incomplete', 'FORGE_SQUADS_MULTISIG and FORGE_SQUADS_SPENDING_LIMIT must be set together');
182
+ }
183
+ const multisig = readOptionalAddress('FORGE_SQUADS_MULTISIG', env);
184
+ const spendingLimit = readOptionalAddress('FORGE_SQUADS_SPENDING_LIMIT', env);
185
+ let vaultIndex = 0;
186
+ if (indexRaw !== '') {
187
+ if (!VAULT_INDEX_RE.test(indexRaw)) {
188
+ throw new ForgeSquadsConfigError('invalid_index', 'FORGE_SQUADS_VAULT_INDEX must be an integer between 0 and 255');
189
+ }
190
+ vaultIndex = Number(indexRaw);
191
+ }
192
+ if (spendingLimit.equals(multisig)) {
193
+ throw new ForgeSquadsConfigError('not_distinct', 'FORGE_SQUADS_SPENDING_LIMIT must be a distinct account');
194
+ }
195
+ return { multisig, spendingLimit, vaultIndex };
196
+ }
package/dist/claims.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { PublicKey, Transaction, type Commitment, type Connection } from '@solana/web3.js';
2
2
  import { type AddressEnv } from './addresses.js';
3
- import { type RetryOptions, type TokenAmounts } from './reads.js';
3
+ import { type DbcPoolState, type RetryOptions, type TokenAmounts } from './reads.js';
4
4
  /** Base fee per signature (lamports); claims set no priority fee. */
5
5
  export declare const CLAIM_LAMPORTS_PER_SIGNATURE = 5000n;
6
6
  export { PARTNER_POOL_CREATION_FEE_CLAIMED_BIT, partnerPoolCreationFeeShare } from './reads.js';
7
- export type ClaimErrorCode = 'not_pool_creator' | 'not_fee_claimer' | 'unsupported_quote_mint' | 'invalid_receiver' | 'receiver_mismatch' | 'receiver_not_rent_exempt' | 'claimer_wsol_not_empty' | 'unexpected_sdk_instructions';
7
+ export type ClaimErrorCode = 'not_pool_creator' | 'not_fee_claimer' | 'unsupported_quote_mint' | 'invalid_receiver' | 'receiver_mismatch' | 'receiver_not_rent_exempt' | 'claimer_wsol_not_empty' | 'unexpected_sdk_instructions' | 'not_migrated' | 'damm_pool_not_found' | 'damm_pool_mismatch' | 'position_not_found' | 'position_mismatch' | 'position_not_owned';
8
8
  export declare class ClaimError extends Error {
9
9
  readonly code: ClaimErrorCode;
10
10
  constructor(code: ClaimErrorCode, message: string);
@@ -43,6 +43,12 @@ export interface ClaimSummary {
43
43
  */
44
44
  receiverQuoteLamports: bigint;
45
45
  quoteIsExact: boolean;
46
+ /**
47
+ * WSOL the claimer's temporary account already held when the transaction was built and that
48
+ * the close hands to the receiver (counted in `receiverQuoteLamports`). Zero when the account
49
+ * is empty or new, and when the close returns it to a claimer paying its own claim.
50
+ */
51
+ existingWsolLamports: bigint;
46
52
  /** Zero costs when there is no transaction. */
47
53
  costs: ClaimCosts;
48
54
  /**
@@ -100,6 +106,10 @@ export declare function minimumWorthwhileClaimLamports(costs: ClaimCosts, minNet
100
106
  * callers skip claims below it. Base tokens are not valued (FORGE configs collect SOL fees only).
101
107
  */
102
108
  export declare function isClaimWorthwhile(summary: ClaimSummary, minNetLamports?: bigint): boolean;
109
+ /** @throws ClaimError `unsupported_quote_mint` unless the pool is quoted in SOL. */
110
+ export declare function assertSolQuotedPool(state: DbcPoolState): void;
111
+ /** @throws ClaimError `invalid_receiver` for anything but a usable wallet address. */
112
+ export declare function assertClaimReceiver(receiver: PublicKey): void;
103
113
  /**
104
114
  * Builds the creator claim of `pool` to `receiver` (see the module comment for the transaction).
105
115
  * @throws ClaimError when `creator` is not the pool creator, the pool is not SOL-quoted, the
package/dist/claims.js CHANGED
@@ -25,8 +25,10 @@
25
25
  * exactly that amount of SOL to the receiver.
26
26
  * So the payer's balance only decreases by the network fee and the one-time rent below, and the
27
27
  * claimer's balance does not change at all when the claimer is not the payer (read-only signer).
28
- * A claimer WSOL account that already holds WSOL is refused when the claimer is not the payer
29
- * (`claimer_wsol_not_empty`): closing it would hand that WSOL to the payer.
28
+ * A claimer WSOL account that already holds WSOL (anyone can send some to it) goes with the
29
+ * close: to the receiver when the receiver pays (accepted and counted in the summary as
30
+ * received, `existingWsolLamports`), back to the claimer when it pays its own claim (accepted).
31
+ * It is refused when the close would hand it to a payer that is neither (`claimer_wsol_not_empty`).
30
32
  *
31
33
  * Base side (the coin): the program needs a base token account even when there is nothing to
32
34
  * claim (FORGE configs collect fees in SOL only). It is the receiver's associated account, created
@@ -66,12 +68,15 @@ export function minimumWorthwhileClaimLamports(costs, minNetLamports = 0n) {
66
68
  export function isClaimWorthwhile(summary, minNetLamports = 0n) {
67
69
  return summary.receiverQuoteLamports > 0n && summary.netLamports > minNetLamports;
68
70
  }
69
- function assertSolQuote(state) {
71
+ /** @throws ClaimError `unsupported_quote_mint` unless the pool is quoted in SOL. */
72
+ export function assertSolQuotedPool(state) {
70
73
  if (!state.config.quoteMint.equals(SOL_QUOTE_MINT)) {
71
74
  throw new ClaimError('unsupported_quote_mint', `Pool ${state.address.toBase58()} is not quoted in SOL: FORGE pools are SOL-quoted only`);
72
75
  }
73
76
  }
74
- function assertReceiver(receiver) {
77
+ const assertSolQuote = assertSolQuotedPool;
78
+ /** @throws ClaimError `invalid_receiver` for anything but a usable wallet address. */
79
+ export function assertClaimReceiver(receiver) {
75
80
  if (!(receiver instanceof PublicKey)) {
76
81
  throw new ClaimError('invalid_receiver', 'Claim receiver must be a PublicKey');
77
82
  }
@@ -79,6 +84,7 @@ function assertReceiver(receiver) {
79
84
  throw new ClaimError('invalid_receiver', 'Claim receiver is not a valid wallet');
80
85
  }
81
86
  }
87
+ const assertReceiver = assertClaimReceiver;
82
88
  function isEmpty(amounts) {
83
89
  return amounts.base === 0n && amounts.quote === 0n;
84
90
  }
@@ -111,7 +117,13 @@ async function buildTradingClaim(connection, input, options) {
111
117
  transaction: null,
112
118
  lastValidBlockHeight: null,
113
119
  skipReason: 'nothing_to_claim',
114
- summary: { ...baseSummary, receiverQuoteLamports: 0n, costs: ZERO_COSTS, netLamports: 0n },
120
+ summary: {
121
+ ...baseSummary,
122
+ receiverQuoteLamports: 0n,
123
+ existingWsolLamports: 0n,
124
+ costs: ZERO_COSTS,
125
+ netLamports: 0n,
126
+ },
115
127
  };
116
128
  }
117
129
  const baseMint = state.virtualPool.poolState.baseMint;
@@ -136,9 +148,11 @@ async function buildTradingClaim(connection, input, options) {
136
148
  retryRpc(() => connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE, commitment), options.retry),
137
149
  ]);
138
150
  const [receiverBaseInfo, claimerWsolInfo, receiverInfo] = infos;
139
- // The temporary WSOL account is closed to the payer: when it already holds WSOL of a claimer
140
- // that is not the payer, that WSOL would move to the payer. Refuse instead.
141
- if (claimerWsolInfo && !claimer.equals(payer) && claimerWsolInfo.owner.equals(TOKEN_PROGRAM_ID)) {
151
+ // The temporary WSOL account is closed to the payer. WSOL it already holds (anyone can send
152
+ // some, 1 lamport is enough) goes with the close: fine when the payer is the receiver (it is
153
+ // received) or the claimer itself (its own); refused when it would reach a third-party payer.
154
+ let existingWsolLamports = 0n;
155
+ if (claimerWsolInfo && claimerWsolInfo.owner.equals(TOKEN_PROGRAM_ID)) {
142
156
  let amount;
143
157
  try {
144
158
  amount = unpackAccount(claimerWsol, claimerWsolInfo, TOKEN_PROGRAM_ID).amount;
@@ -147,9 +161,11 @@ async function buildTradingClaim(connection, input, options) {
147
161
  // Not a token account (e.g. lamports sent to the address): the idempotent creation takes it.
148
162
  amount = 0n;
149
163
  }
150
- if (amount > 0n) {
164
+ if (amount > 0n && !claimer.equals(payer) && !receiver.equals(payer)) {
151
165
  throw new ClaimError('claimer_wsol_not_empty', `The claimer's WSOL account ${claimerWsol.toBase58()} holds ${amount} lamports of WSOL: unwrap it before claiming with another payer`);
152
166
  }
167
+ if (receiver.equals(payer))
168
+ existingWsolLamports = amount;
153
169
  }
154
170
  if (exact && quote > 0n) {
155
171
  const size = receiverInfo?.data.length ?? 0;
@@ -189,11 +205,13 @@ async function buildTradingClaim(connection, input, options) {
189
205
  payerTotalLamports,
190
206
  payerRequiredBalanceLamports: payerTotalLamports + temporaryLamports,
191
207
  };
208
+ const receiverQuoteLamports = quote + existingWsolLamports;
192
209
  const summary = {
193
210
  ...baseSummary,
194
- receiverQuoteLamports: quote,
211
+ receiverQuoteLamports,
212
+ existingWsolLamports,
195
213
  costs,
196
- netLamports: quote - payerTotalLamports,
214
+ netLamports: receiverQuoteLamports - payerTotalLamports,
197
215
  };
198
216
  if (options.minNetLamports !== undefined && !isClaimWorthwhile(summary, options.minNetLamports)) {
199
217
  return { transaction: null, lastValidBlockHeight: null, skipReason: 'not_worthwhile', summary };
package/dist/config.d.ts CHANGED
@@ -25,41 +25,10 @@
25
25
  * `buildLaunchpadConfig` / `buildLaunchpadCoinConfig` are pure and deterministic. Every result
26
26
  * passes `validate.ts` before it is returned; otherwise a `ValidationError` is thrown.
27
27
  */
28
- import { ActivationType, BaseFeeMode, MigrationFeeOption, MigrationOption, TokenAuthorityOption, TokenDecimal, TokenType, type BuildCurveParams, type ConfigParameters, type SwapQuoteConfig } from '@meteora-ag/dynamic-bonding-curve-sdk';
28
+ import { type BuildCurveParams, type ConfigParameters, type PoolConfig, type SwapQuoteConfig } from '@meteora-ag/dynamic-bonding-curve-sdk';
29
29
  import { PublicKey, Transaction, type Connection } from '@solana/web3.js';
30
30
  import { LaunchpadSpec } from '@forge/shared';
31
- /** Token of every coin: SPL token, 1,000,000,000 tokens with 6 decimals, immutable metadata. */
32
- export declare const FORGE_TOKEN_SETTINGS: {
33
- readonly tokenType: TokenType.SPLToken;
34
- readonly tokenBaseDecimal: TokenDecimal.SIX;
35
- readonly tokenQuoteDecimal: 9;
36
- readonly tokenAuthorityOption: TokenAuthorityOption.Immutable;
37
- readonly totalTokenSupply: 1000000000;
38
- readonly leftover: 0;
39
- };
40
- /**
41
- * Anti-sniper fee schedule (`spec.antiSniper`): the fee starts at `startingFeeBps` and decreases
42
- * linearly, in `numberOfPeriod` equal steps over `totalDurationSeconds`, to `spec.tradingFeeBps`.
43
- * Within the validate.ts bounds (starting fee <= 9900 bps, duration <= 300 s).
44
- */
45
- export declare const ANTI_SNIPER_FEE_SCHEDULE: {
46
- readonly baseFeeMode: BaseFeeMode.FeeSchedulerLinear;
47
- readonly startingFeeBps: 9900;
48
- readonly numberOfPeriod: 10;
49
- readonly totalDurationSeconds: 60;
50
- };
51
- /** Migration: DAMM v2, migrated pool fee 0.25%, no migration fee, 20% of the supply migrated. */
52
- export declare const FORGE_MIGRATION_SETTINGS: {
53
- readonly migrationOption: MigrationOption.MET_DAMM_V2;
54
- readonly migrationFeeOption: MigrationFeeOption.FixedBps25;
55
- readonly migrationFee: {
56
- readonly feePercentage: 0;
57
- readonly creatorFeePercentage: 0;
58
- };
59
- readonly percentageSupplyOnMigration: 20;
60
- };
61
- /** Fee scheduler periods are counted in seconds. */
62
- export declare const FORGE_ACTIVATION_TYPE = ActivationType.Timestamp;
31
+ export { ANTI_SNIPER_FEE_SCHEDULE, FORGE_ACTIVATION_TYPE, FORGE_MIGRATION_SETTINGS, FORGE_TOKEN_SETTINGS, FORGE_TOKEN_SUPPLY_RAW, forgeBaseFee, forgeFeeSchedulerParam, forgeReferenceCurve, type ForgeReferenceCurve, } from './settings.js';
63
32
  /**
64
33
  * Pool creation fee of the launchpad coin config: none. The only pool ever created on it is the
65
34
  * client's own coin, paid by the client: a fee would only move the client's SOL to its own fee
@@ -69,9 +38,14 @@ export declare const FORGE_ACTIVATION_TYPE = ActivationType.Timestamp;
69
38
  */
70
39
  export declare const LAUNCHPAD_COIN_POOL_CREATION_FEE_SOL = 0;
71
40
  /** Pool creation fee of a config kind, in SOL. */
72
- export declare function poolCreationFeeSolFor(spec: LaunchpadSpec, kind: ConfigKind): number;
41
+ export declare function poolCreationFeeSolFor(spec: Pick<LaunchpadSpec, 'poolCreationFeeSol'>, kind: ConfigKind): number;
73
42
  /** Which of the two configs. */
74
43
  export type ConfigKind = 'launchpad' | 'launchpadCoin';
44
+ /**
45
+ * The spec fields a config depends on, besides the owner wallet (a full `LaunchpadSpec` fits).
46
+ * Everything else in a FORGE config is a fixed setting (settings.ts).
47
+ */
48
+ export type ConfigSpecFields = Pick<LaunchpadSpec, 'tradingFeeBps' | 'antiSniper' | 'coinCreatorSharePct' | 'poolCreationFeeSol'>;
75
49
  /**
76
50
  * What `createConfig` needs besides the config keypair and the payer: the SDK
77
51
  * `ConfigParameters` plus the accounts that decide where the money goes.
@@ -92,9 +66,9 @@ export type LaunchpadConfigParams = ConfigParameters & {
92
66
  */
93
67
  export declare function parseLaunchpadSpec(spec: unknown): LaunchpadSpec;
94
68
  /** Creator share of the trading fees for each config kind. */
95
- export declare function creatorTradingFeePercentageFor(spec: LaunchpadSpec, kind: ConfigKind): number;
96
- /** The `buildCurve` input for a spec and a config kind (pure). */
97
- export declare function curveParamsFor(spec: LaunchpadSpec, kind: ConfigKind): BuildCurveParams;
69
+ export declare function creatorTradingFeePercentageFor(spec: Pick<LaunchpadSpec, 'coinCreatorSharePct'>, kind: ConfigKind): number;
70
+ /** The `buildCurve` input for a spec (or just its config fields) and a config kind (pure). */
71
+ export declare function curveParamsFor(spec: ConfigSpecFields, kind: ConfigKind): BuildCurveParams;
98
72
  export interface ConfigParamsOptions {
99
73
  /**
100
74
  * Metadata URI the launchpad coin will be launched with, when known (the signer knows it from
@@ -115,6 +89,35 @@ export declare function buildConfigParams(spec: LaunchpadSpec, kind: ConfigKind,
115
89
  export declare function buildLaunchpadConfig(spec: LaunchpadSpec): LaunchpadConfigParams;
116
90
  /** Launchpad coin config: the launchpad's own coin, FORGE is the creator (25%). */
117
91
  export declare function buildLaunchpadCoinConfig(spec: LaunchpadSpec): LaunchpadConfigParams;
92
+ /** What an on-chain config is expected to be, besides its kind. */
93
+ export interface OnchainConfigExpectation {
94
+ /**
95
+ * The spec the config was built from. Every field is then compared with
96
+ * `buildConfigParams(spec, kind)`, exactly what `buildCreateConfigTx` sent.
97
+ */
98
+ spec?: LaunchpadSpec;
99
+ /** Fee claimer and leftover receiver (`spec.ownerWallet` when a spec is given). */
100
+ owner?: PublicKey;
101
+ /** Pool creation fee in lamports (launchpad config; the coin config never has one). */
102
+ poolCreationFeeLamports?: bigint;
103
+ }
104
+ /**
105
+ * Compares a config read from the chain with the parameters core builds for it, field by field:
106
+ * accounts, token, base fee (cliff, periods, period, reduction, mode), dynamic fee, creator share,
107
+ * pool creation fee, first swap at the minimum fee, activation, liquidity split and vesting,
108
+ * migration (option, fee option, threshold, migration fee percentages, migrated pool fee), locked
109
+ * vesting, token supply, start price and curve. The values the program derives from these
110
+ * (`swapBaseAmount`, `migrationBaseThreshold`, `migrationSqrtPrice`) are not compared.
111
+ *
112
+ * With `expected.spec`, the reference is `buildConfigParams(spec, kind)`. Without it, the spec
113
+ * fields are read from the config itself (`inferSpecFields`: they must be values a FORGE spec can
114
+ * hold) and every other field must be FORGE's fixed setting; the fee claimer and leftover receiver
115
+ * must be `expected.owner` (or, without it, the same key).
116
+ *
117
+ * @returns every mismatch, as readable messages (on-chain values are public); empty when the config
118
+ * is exactly a FORGE config of this kind.
119
+ */
120
+ export declare function onchainConfigMismatches(config: PoolConfig, kind: ConfigKind, expected?: OnchainConfigExpectation): string[];
118
121
  export interface FirstBuyQuote {
119
122
  /** Raw base units (6 decimals) delivered by the first buy. */
120
123
  tokensOut: bigint;