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