@forgedevv/core 0.2.0 → 0.3.1
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 +152 -2
- package/dist/addresses.d.ts +25 -6
- package/dist/addresses.js +27 -5
- package/dist/claims.d.ts +31 -4
- package/dist/claims.js +46 -5
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -5
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +20 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +12 -6
- package/dist/jupiter-referral.d.ts +219 -4
- package/dist/jupiter-referral.js +384 -13
- package/dist/launch-metadata.d.ts +2 -0
- package/dist/launch-metadata.js +2 -0
- package/dist/meteora-referral.d.ts +119 -0
- package/dist/meteora-referral.js +184 -0
- package/dist/payments.d.ts +17 -0
- package/dist/payments.js +20 -2
- package/dist/pool.d.ts +40 -0
- package/dist/pool.js +128 -24
- package/dist/position-claims.d.ts +9 -1
- package/dist/position-claims.js +21 -4
- package/dist/priority-fee.d.ts +202 -0
- package/dist/priority-fee.js +322 -0
- package/dist/reads.d.ts +20 -0
- package/dist/reads.js +39 -2
- package/dist/swap.d.ts +35 -16
- package/dist/swap.js +68 -19
- package/dist/tx.d.ts +2 -0
- package/dist/tx.js +4 -2
- package/package.json +3 -2
package/dist/jupiter-referral.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Jupiter referral fee claim (docs/METEORA.md "The platform fee", after graduation; D7, D8).
|
|
3
3
|
*
|
|
4
|
-
* After a coin graduates, trades go through Jupiter and FORGE's
|
|
4
|
+
* After a coin graduates, trades go through Jupiter and FORGE's 50 bps integrator fee is collected
|
|
5
5
|
* by the Jupiter referral program into token accounts of FORGE's referral account
|
|
6
6
|
* (`FORGE_JUPITER_REFERRAL_ACCOUNT`), one per currency Jupiter collects in. Claiming is
|
|
7
7
|
* permissionless: it moves a token account's balance to the referral account's `partner` (the
|
|
8
|
-
* owner of the referral account: the FORGE
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
8
|
+
* owner of the referral account: the FORGE revenue wallet since D34), minus the project's share.
|
|
9
|
+
* The revenue job of the signer then converts the balances to SOL and forwards them to the vault,
|
|
10
|
+
* which feeds the buyback (D8). The fee payer only pays the network fee and, the first time, the
|
|
11
|
+
* rent of the partner's token account.
|
|
11
12
|
*
|
|
12
13
|
* Built from the referral program's IDL (program `REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3`,
|
|
13
14
|
* `@jup-ag/referral-sdk` 0.3.0) with raw instructions: the SDK pulls Anchor, too heavy for the
|
|
@@ -16,23 +17,45 @@
|
|
|
16
17
|
* - `claimV2`: the associated token account of the referral account (classic or Token-2022).
|
|
17
18
|
* Account order, PDA seeds and Anchor discriminators are the IDL's; the unit tests pin them.
|
|
18
19
|
*
|
|
20
|
+
* The Jupiter Plugin (Ultra) only pays referral accounts of Jupiter's Ultra project
|
|
21
|
+
* (`JUPITER_ULTRA_REFERRAL_PROJECT`), into their v2 collection accounts. `claim_v2` only accepts a
|
|
22
|
+
* NAMED referral account at the PDA `["referral", project, name]` (created with
|
|
23
|
+
* `initialize_referral_account_with_name`): an unnamed one (plain `initialize_referral_account`)
|
|
24
|
+
* can never claim them. So the builder refuses a referral account of another project
|
|
25
|
+
* (`project_mismatch`; `expectedProject` overrides the Ultra project), and builds a v2 claim only
|
|
26
|
+
* for a named PDA (`referral_account_unnamed` or `referral_account_not_pda` otherwise).
|
|
27
|
+
*
|
|
19
28
|
* 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
|
-
*
|
|
22
|
-
*
|
|
29
|
+
* builder checks the connection's genesis hash and refuses any other cluster (`wrong_cluster`)
|
|
30
|
+
* before it reads any account, so the project and name checks never run on devnet (where neither
|
|
31
|
+
* the Ultra project nor Jupiter's fee collection exist): the devnet behaviour is unchanged.
|
|
32
|
+
* The partner can be asserted (`expectedPartner` = the revenue wallet): a referral account owned by
|
|
33
|
+
* anyone else is refused (`partner_mismatch`), fees would not reach the revenue wallet.
|
|
34
|
+
*
|
|
35
|
+
* The last section builds the one-off SETUP of that referral account (the signer's
|
|
36
|
+
* `--setup-revenue-referrals` command): `initialize_referral_account_with_name` under the Ultra
|
|
37
|
+
* project with the revenue wallet as partner, and the v2 collection accounts of the currencies
|
|
38
|
+
* Jupiter collects in. Only the payer signs; the program facts are listed there.
|
|
23
39
|
*/
|
|
24
|
-
import {
|
|
40
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
25
41
|
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, unpackAccount, } from '@solana/spl-token';
|
|
26
42
|
import { PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '@solana/web3.js';
|
|
27
43
|
import { PaymentError, assertCluster } from './payments.js';
|
|
44
|
+
import { estimatePriorityFee, priorityFeeInstructions, resolvePriorityFeePolicy, writableAccountsOf, } from './priority-fee.js';
|
|
28
45
|
import { retryRpc } from './reads.js';
|
|
29
46
|
// ---------------------------------------------------------------------------------------------
|
|
30
47
|
// Constants (IDL)
|
|
31
48
|
// ---------------------------------------------------------------------------------------------
|
|
32
49
|
/** Jupiter referral program (mainnet-beta). */
|
|
33
50
|
export const JUPITER_REFERRAL_PROGRAM_ID = new PublicKey('REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3');
|
|
51
|
+
/**
|
|
52
|
+
* Jupiter's Ultra project in the referral program (mainnet-beta). The Jupiter Plugin and Ultra
|
|
53
|
+
* `/order` only collect the referral fee into referral accounts of this project.
|
|
54
|
+
* @see https://developers.jup.ag/docs/ultra/add-fees-to-ultra.md
|
|
55
|
+
*/
|
|
56
|
+
export const JUPITER_ULTRA_REFERRAL_PROJECT = new PublicKey('DkiqsTrw1u1bYFumumC7sCG2S8K25qc2vemJFHyW2wJc');
|
|
34
57
|
function anchorDiscriminator(name) {
|
|
35
|
-
return
|
|
58
|
+
return Buffer.from(sha256(new TextEncoder().encode(name))).subarray(0, 8);
|
|
36
59
|
}
|
|
37
60
|
export const JUPITER_REFERRAL_ACCOUNT_DISCRIMINATOR = anchorDiscriminator('account:ReferralAccount');
|
|
38
61
|
export const JUPITER_PROJECT_DISCRIMINATOR = anchorDiscriminator('account:Project');
|
|
@@ -112,6 +135,13 @@ export function decodeJupiterProject(data) {
|
|
|
112
135
|
export function deriveJupiterReferralTokenAccount(referralAccount, mint, programId = JUPITER_REFERRAL_PROGRAM_ID) {
|
|
113
136
|
return PublicKey.findProgramAddressSync([Buffer.from('referral_ata', 'utf8'), referralAccount.toBuffer(), mint.toBuffer()], programId)[0];
|
|
114
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Named referral account: PDA `["referral", project, name]` (`initialize_referral_account_with_name`,
|
|
140
|
+
* and the signer seeds of `claim_v2`). Throws when the name is longer than a seed (32 bytes).
|
|
141
|
+
*/
|
|
142
|
+
export function deriveJupiterReferralAccount(project, name, programId = JUPITER_REFERRAL_PROGRAM_ID) {
|
|
143
|
+
return PublicKey.findProgramAddressSync([Buffer.from('referral', 'utf8'), project.toBuffer(), Buffer.from(name, 'utf8')], programId)[0];
|
|
144
|
+
}
|
|
115
145
|
/** V2 referral token account: the associated token account of the referral account. */
|
|
116
146
|
export function deriveJupiterReferralTokenAccountV2(referralAccount, mint, tokenProgram) {
|
|
117
147
|
return getAssociatedTokenAddressSync(mint, referralAccount, true, tokenProgram);
|
|
@@ -175,6 +205,11 @@ export function buildJupiterCreateAdminTokenAccountIx(accounts, programId = JUPI
|
|
|
175
205
|
data: Buffer.from(JUPITER_CREATE_ADMIN_TOKEN_ACCOUNT_DISCRIMINATOR),
|
|
176
206
|
});
|
|
177
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Compute unit limit of a Jupiter referral claim with a priority fee: up to two token account
|
|
210
|
+
* creations and two claims (each a token transfer pair), with a wide margin.
|
|
211
|
+
*/
|
|
212
|
+
export const JUPITER_REFERRAL_CLAIM_COMPUTE_UNITS = 300_000;
|
|
178
213
|
function tokenAmount(address, info, tokenProgram) {
|
|
179
214
|
if (!info || !info.owner.equals(tokenProgram))
|
|
180
215
|
return 0n;
|
|
@@ -186,15 +221,40 @@ function tokenAmount(address, info, tokenProgram) {
|
|
|
186
221
|
return 0n;
|
|
187
222
|
}
|
|
188
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* `claim_v2` signs with the PDA `["referral", project, name]`: refuses (typed error) a referral
|
|
226
|
+
* account without a name, or one that is not that PDA, before a v2 claim is built.
|
|
227
|
+
*/
|
|
228
|
+
function assertClaimableV2(referralAccount, referral, programId) {
|
|
229
|
+
if (referral.name === null) {
|
|
230
|
+
throw new JupiterReferralError('referral_account_unnamed', 'the referral account has no name: claim_v2 needs a referral account created with a name ' +
|
|
231
|
+
'(PDA ["referral", project, name]), so its v2 collection account cannot be claimed');
|
|
232
|
+
}
|
|
233
|
+
let expected;
|
|
234
|
+
try {
|
|
235
|
+
expected = deriveJupiterReferralAccount(referral.project, referral.name, programId);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new JupiterReferralError('referral_account_not_pda', 'the referral account name is not a valid PDA seed, so claim_v2 cannot sign for it', { cause: error });
|
|
239
|
+
}
|
|
240
|
+
if (!expected.equals(referralAccount)) {
|
|
241
|
+
throw new JupiterReferralError('referral_account_not_pda', 'the referral account is not the PDA ["referral", project, name] that claim_v2 signs with');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
189
244
|
/**
|
|
190
245
|
* Reads the referral account, its project, the mint and the collection accounts, and builds the
|
|
191
246
|
* claim(s) of every funded collection account of `mint` to the partner's associated token account.
|
|
192
247
|
* Mainnet-beta only (genesis hash). Returns `transaction: null` when nothing is claimable.
|
|
248
|
+
* The referral account must belong to `expectedProject` (default: Jupiter's Ultra project), and a
|
|
249
|
+
* funded v2 collection account is only claimed for a named referral PDA (typed errors otherwise).
|
|
193
250
|
*/
|
|
194
251
|
export async function buildJupiterReferralClaimTx(p) {
|
|
195
252
|
const { connection } = p;
|
|
196
253
|
const commitment = p.commitment ?? 'confirmed';
|
|
197
254
|
const programId = p.programId ?? JUPITER_REFERRAL_PROGRAM_ID;
|
|
255
|
+
if (p.priority !== undefined)
|
|
256
|
+
resolvePriorityFeePolicy(p.priority);
|
|
257
|
+
const expectedProject = p.expectedProject ?? JUPITER_ULTRA_REFERRAL_PROJECT;
|
|
198
258
|
const retry = p.retry ?? {};
|
|
199
259
|
if (p.payer.equals(p.referralAccount) || p.mint.equals(p.referralAccount)) {
|
|
200
260
|
throw new JupiterReferralError('invalid_params', 'payer, mint and referral account must differ');
|
|
@@ -219,6 +279,10 @@ export async function buildJupiterReferralClaimTx(p) {
|
|
|
219
279
|
if (p.expectedPartner && !referral.partner.equals(p.expectedPartner)) {
|
|
220
280
|
throw new JupiterReferralError('partner_mismatch', 'the referral account partner is not the expected receiver');
|
|
221
281
|
}
|
|
282
|
+
if (!referral.project.equals(expectedProject)) {
|
|
283
|
+
throw new JupiterReferralError('project_mismatch', `the referral account belongs to project ${referral.project.toBase58()}, not to the ` +
|
|
284
|
+
`expected project ${expectedProject.toBase58()} (Jupiter Ultra by default)`);
|
|
285
|
+
}
|
|
222
286
|
if (!mintInfo)
|
|
223
287
|
throw new JupiterReferralError('mint_not_found', 'mint not found');
|
|
224
288
|
const tokenProgram = mintInfo.owner;
|
|
@@ -242,8 +306,10 @@ export async function buildJupiterReferralClaimTx(p) {
|
|
|
242
306
|
if (v1Amount > 0n)
|
|
243
307
|
claims.push({ version: 'v1', referralTokenAccount: v1Account, amount: v1Amount });
|
|
244
308
|
const v2Amount = tokenAmount(v2Account, v2Info ?? null, tokenProgram);
|
|
245
|
-
if (v2Amount > 0n)
|
|
309
|
+
if (v2Amount > 0n) {
|
|
310
|
+
assertClaimableV2(p.referralAccount, referral, programId);
|
|
246
311
|
claims.push({ version: 'v2', referralTokenAccount: v2Account, amount: v2Amount });
|
|
312
|
+
}
|
|
247
313
|
const projectAuthority = deriveJupiterProjectAuthority(project.base, programId);
|
|
248
314
|
const summary = {
|
|
249
315
|
referralAccount: p.referralAccount,
|
|
@@ -258,8 +324,9 @@ export async function buildJupiterReferralClaimTx(p) {
|
|
|
258
324
|
createsPartnerTokenAccount: claims.length > 0 && !partnerInfo,
|
|
259
325
|
createsAdminTokenAccount: claims.length > 0 && !adminInfo,
|
|
260
326
|
};
|
|
261
|
-
if (claims.length === 0)
|
|
262
|
-
return { transaction: null, lastValidBlockHeight: null, summary };
|
|
327
|
+
if (claims.length === 0) {
|
|
328
|
+
return { transaction: null, lastValidBlockHeight: null, summary, priorityFee: null };
|
|
329
|
+
}
|
|
263
330
|
const transaction = new Transaction();
|
|
264
331
|
if (summary.createsPartnerTokenAccount) {
|
|
265
332
|
transaction.add(createAssociatedTokenAccountIdempotentInstruction(p.payer, partnerTokenAccount, referral.partner, p.mint, tokenProgram));
|
|
@@ -288,8 +355,312 @@ export async function buildJupiterReferralClaimTx(p) {
|
|
|
288
355
|
tokenProgram,
|
|
289
356
|
}, claim.version, programId));
|
|
290
357
|
}
|
|
358
|
+
let priorityFee = null;
|
|
359
|
+
if (p.priority !== undefined) {
|
|
360
|
+
priorityFee = await estimatePriorityFee({
|
|
361
|
+
connection,
|
|
362
|
+
writableAccounts: writableAccountsOf(transaction.instructions),
|
|
363
|
+
computeUnitLimit: JUPITER_REFERRAL_CLAIM_COMPUTE_UNITS,
|
|
364
|
+
setting: p.priority,
|
|
365
|
+
});
|
|
366
|
+
transaction.instructions.unshift(...priorityFeeInstructions(priorityFee));
|
|
367
|
+
}
|
|
291
368
|
const latest = await retryRpc(() => connection.getLatestBlockhash(commitment), retry);
|
|
292
369
|
transaction.feePayer = p.payer;
|
|
293
370
|
transaction.recentBlockhash = latest.blockhash;
|
|
294
|
-
return { transaction, lastValidBlockHeight: latest.lastValidBlockHeight, summary };
|
|
371
|
+
return { transaction, lastValidBlockHeight: latest.lastValidBlockHeight, summary, priorityFee };
|
|
372
|
+
}
|
|
373
|
+
// ---------------------------------------------------------------------------------------------
|
|
374
|
+
// Setup of FORGE's referral account (pure builders; docs/MAINNET.md section 5)
|
|
375
|
+
// ---------------------------------------------------------------------------------------------
|
|
376
|
+
//
|
|
377
|
+
// Verified against the program source (github.com/TeamRaccoons/referral, program/programs/
|
|
378
|
+
// referral/src/instructions/initialize_referral_account_with_name.rs, lib.rs, claim_v2.rs; the
|
|
379
|
+
// IDL of @jup-ag/referral-sdk 0.3.0 agrees):
|
|
380
|
+
// - `initialize_referral_account_with_name(params: { name: String })`: accounts `payer` (mut,
|
|
381
|
+
// signer), `partner` (SystemAccount, read-only, NOT a signer), `project` (read-only, PDA
|
|
382
|
+
// `["project", base]`), `referral_account` (mut, init, PDA `["referral", project, name]`,
|
|
383
|
+
// `ReferralAccount::LEN` = 274 bytes, rent paid by `payer`), `system_program`. The share is the
|
|
384
|
+
// project's `default_share_bps`. Name: `params.name.chars().count() < 20`, so 1 to 19
|
|
385
|
+
// characters; the name is also a PDA seed, so at most 32 UTF-8 bytes.
|
|
386
|
+
// - The v2 collection accounts `claim_v2` empties are the associated token accounts of the
|
|
387
|
+
// referral account (`associated_token::authority = referral_account`): the SDK's
|
|
388
|
+
// `initializeReferralTokenAccountV2` is a plain `CreateIdempotent` of the Associated Token
|
|
389
|
+
// program, paid by the payer, which is what `buildJupiterReferralTokenAccountV2Ix` builds.
|
|
390
|
+
// Only the payer signs anywhere: the partner (the revenue wallet) never does.
|
|
391
|
+
export const JUPITER_INITIALIZE_REFERRAL_ACCOUNT_WITH_NAME_DISCRIMINATOR = anchorDiscriminator('global:initialize_referral_account_with_name');
|
|
392
|
+
/** `ReferralAccount::LEN` = 8 + 32 + 32 + 200 + 2: what the initialization allocates. */
|
|
393
|
+
export const JUPITER_REFERRAL_ACCOUNT_SIZE = 274;
|
|
394
|
+
/** `params.name.chars().count() < 20` in the program. */
|
|
395
|
+
export const JUPITER_REFERRAL_NAME_MAX_CHARS = 19;
|
|
396
|
+
/** The name is a PDA seed: at most 32 bytes. */
|
|
397
|
+
export const JUPITER_REFERRAL_NAME_MAX_BYTES = 32;
|
|
398
|
+
/**
|
|
399
|
+
* Compute unit limit of the setup transaction with a priority fee: one Anchor account
|
|
400
|
+
* initialization and up to a few associated token account creations, with a wide margin.
|
|
401
|
+
*/
|
|
402
|
+
export const JUPITER_REFERRAL_SETUP_COMPUTE_UNITS = 150_000;
|
|
403
|
+
/**
|
|
404
|
+
* Checks a referral account name: 1 to 19 characters (code points, the program's rule), at most
|
|
405
|
+
* 32 UTF-8 bytes (a PDA seed), and FORGE's own restriction to printable characters without
|
|
406
|
+
* whitespace (a name goes into environment files and documentation). Returns the name.
|
|
407
|
+
* @throws JupiterReferralError `invalid_params`
|
|
408
|
+
*/
|
|
409
|
+
export function assertJupiterReferralName(name) {
|
|
410
|
+
const fail = (reason) => new JupiterReferralError('invalid_params', reason);
|
|
411
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
412
|
+
throw fail('the name must be a non-empty string');
|
|
413
|
+
}
|
|
414
|
+
const chars = [...name].length;
|
|
415
|
+
if (chars > JUPITER_REFERRAL_NAME_MAX_CHARS) {
|
|
416
|
+
throw fail(`the name has ${chars} characters, at most ${JUPITER_REFERRAL_NAME_MAX_CHARS} are allowed`);
|
|
417
|
+
}
|
|
418
|
+
const bytes = Buffer.byteLength(name, 'utf8');
|
|
419
|
+
if (bytes > JUPITER_REFERRAL_NAME_MAX_BYTES) {
|
|
420
|
+
throw fail(`the name is ${bytes} bytes, at most ${JUPITER_REFERRAL_NAME_MAX_BYTES} are allowed`);
|
|
421
|
+
}
|
|
422
|
+
if (/[\s\p{Cc}\p{Cf}]/u.test(name)) {
|
|
423
|
+
throw fail('the name must not contain whitespace or control characters');
|
|
424
|
+
}
|
|
425
|
+
return name;
|
|
426
|
+
}
|
|
427
|
+
/** Borsh `String`: u32 little-endian byte length, then the UTF-8 bytes. */
|
|
428
|
+
export function encodeJupiterReferralName(name) {
|
|
429
|
+
const bytes = Buffer.from(assertJupiterReferralName(name), 'utf8');
|
|
430
|
+
const length = Buffer.alloc(4);
|
|
431
|
+
length.writeUInt32LE(bytes.length);
|
|
432
|
+
return Buffer.concat([length, bytes]);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* `initialize_referral_account_with_name`: the IDL's accounts in the IDL's order, the Anchor
|
|
436
|
+
* discriminator then the Borsh-encoded `{ name }`. Refuses a referral account that is not the
|
|
437
|
+
* PDA of the project and the name, and a payer, partner, project or referral account that
|
|
438
|
+
* coincide.
|
|
439
|
+
*/
|
|
440
|
+
export function buildJupiterInitializeReferralAccountWithNameIx(accounts, name, programId = JUPITER_REFERRAL_PROGRAM_ID) {
|
|
441
|
+
const data = encodeJupiterReferralName(name);
|
|
442
|
+
const expected = deriveJupiterReferralAccount(accounts.project, name, programId);
|
|
443
|
+
if (!expected.equals(accounts.referralAccount)) {
|
|
444
|
+
throw new JupiterReferralError('invalid_params', 'referralAccount is not the PDA ["referral", project, name] of that name');
|
|
445
|
+
}
|
|
446
|
+
const keys = [accounts.payer, accounts.partner, accounts.project, accounts.referralAccount];
|
|
447
|
+
if (new Set(keys.map((k) => k.toBase58())).size !== keys.length) {
|
|
448
|
+
throw new JupiterReferralError('invalid_params', 'payer, partner, project and referral account must differ');
|
|
449
|
+
}
|
|
450
|
+
return new TransactionInstruction({
|
|
451
|
+
programId,
|
|
452
|
+
keys: [
|
|
453
|
+
{ pubkey: accounts.payer, isSigner: true, isWritable: true },
|
|
454
|
+
ro(accounts.partner),
|
|
455
|
+
ro(accounts.project),
|
|
456
|
+
rw(accounts.referralAccount),
|
|
457
|
+
ro(SystemProgram.programId),
|
|
458
|
+
],
|
|
459
|
+
data: Buffer.concat([JUPITER_INITIALIZE_REFERRAL_ACCOUNT_WITH_NAME_DISCRIMINATOR, data]),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The v2 collection account of `mint` (what the Jupiter Plugin pays and `claim_v2` empties): an
|
|
464
|
+
* idempotent creation of the referral account's associated token account, paid by `payer`
|
|
465
|
+
* (the SDK's `initializeReferralTokenAccountV2`). Idempotent on-chain: an existing account is
|
|
466
|
+
* left as it is.
|
|
467
|
+
*/
|
|
468
|
+
export function buildJupiterReferralTokenAccountV2Ix(p) {
|
|
469
|
+
assertTokenProgram(p.tokenProgram);
|
|
470
|
+
if (p.payer.equals(p.referralAccount) || p.mint.equals(p.referralAccount)) {
|
|
471
|
+
throw new JupiterReferralError('invalid_params', 'payer, mint and referral account must differ');
|
|
472
|
+
}
|
|
473
|
+
const tokenAccount = deriveJupiterReferralTokenAccountV2(p.referralAccount, p.mint, p.tokenProgram);
|
|
474
|
+
return {
|
|
475
|
+
tokenAccount,
|
|
476
|
+
instruction: createAssociatedTokenAccountIdempotentInstruction(p.payer, tokenAccount, p.referralAccount, p.mint, p.tokenProgram),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Unsigned transaction setting up FORGE's Jupiter referral account: the optional compute budget,
|
|
481
|
+
* `initialize_referral_account_with_name` when `createReferralAccount`, then one v2 collection
|
|
482
|
+
* account creation per `tokenAccounts` entry. Refuses an empty plan and a duplicate mint. Does
|
|
483
|
+
* not read the chain: inspect first (`readJupiterReferralSetup`). Mainnet-beta is the only
|
|
484
|
+
* cluster where the program and the Ultra project exist; the read helper enforces it.
|
|
485
|
+
*/
|
|
486
|
+
export function buildJupiterReferralSetupTx(p) {
|
|
487
|
+
const programId = p.programId ?? JUPITER_REFERRAL_PROGRAM_ID;
|
|
488
|
+
const project = p.project ?? JUPITER_ULTRA_REFERRAL_PROJECT;
|
|
489
|
+
const name = assertJupiterReferralName(p.name);
|
|
490
|
+
if (typeof p.blockhash !== 'string' || p.blockhash.length === 0) {
|
|
491
|
+
throw new JupiterReferralError('invalid_params', 'blockhash required');
|
|
492
|
+
}
|
|
493
|
+
if (!Number.isInteger(p.lastValidBlockHeight) || p.lastValidBlockHeight < 0) {
|
|
494
|
+
throw new JupiterReferralError('invalid_params', 'lastValidBlockHeight must be a non-negative integer');
|
|
495
|
+
}
|
|
496
|
+
if (!p.createReferralAccount && p.tokenAccounts.length === 0) {
|
|
497
|
+
throw new JupiterReferralError('invalid_params', 'nothing to set up');
|
|
498
|
+
}
|
|
499
|
+
const seen = new Set();
|
|
500
|
+
for (const { mint } of p.tokenAccounts) {
|
|
501
|
+
if (seen.has(mint.toBase58())) {
|
|
502
|
+
throw new JupiterReferralError('invalid_params', 'duplicate mint in tokenAccounts');
|
|
503
|
+
}
|
|
504
|
+
seen.add(mint.toBase58());
|
|
505
|
+
}
|
|
506
|
+
const referralAccount = deriveJupiterReferralAccount(project, name, programId);
|
|
507
|
+
const transaction = new Transaction({
|
|
508
|
+
feePayer: p.payer,
|
|
509
|
+
blockhash: p.blockhash,
|
|
510
|
+
lastValidBlockHeight: p.lastValidBlockHeight,
|
|
511
|
+
});
|
|
512
|
+
if (p.priorityFee && p.priorityFee.microLamports > 0) {
|
|
513
|
+
transaction.add(...priorityFeeInstructions(p.priorityFee));
|
|
514
|
+
}
|
|
515
|
+
if (p.createReferralAccount) {
|
|
516
|
+
transaction.add(buildJupiterInitializeReferralAccountWithNameIx({ payer: p.payer, partner: p.partner, project, referralAccount }, name, programId));
|
|
517
|
+
}
|
|
518
|
+
const tokenAccounts = [];
|
|
519
|
+
for (const entry of p.tokenAccounts) {
|
|
520
|
+
const built = buildJupiterReferralTokenAccountV2Ix({
|
|
521
|
+
payer: p.payer,
|
|
522
|
+
referralAccount,
|
|
523
|
+
mint: entry.mint,
|
|
524
|
+
tokenProgram: entry.tokenProgram,
|
|
525
|
+
});
|
|
526
|
+
transaction.add(built.instruction);
|
|
527
|
+
tokenAccounts.push({ ...entry, tokenAccount: built.tokenAccount });
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
transaction,
|
|
531
|
+
lastValidBlockHeight: p.lastValidBlockHeight,
|
|
532
|
+
referralAccount,
|
|
533
|
+
project,
|
|
534
|
+
name,
|
|
535
|
+
tokenAccounts,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* What the referral account address holds, against the expected one: absent, ready (a referral
|
|
540
|
+
* account of `project` with `partner` and exactly `name`), or a mismatch with its reason. The
|
|
541
|
+
* caller derives the address as the PDA of the project and the name.
|
|
542
|
+
*/
|
|
543
|
+
export function inspectJupiterReferralAccount(info, expected) {
|
|
544
|
+
if (!info)
|
|
545
|
+
return { status: 'absent' };
|
|
546
|
+
const programId = expected.programId ?? JUPITER_REFERRAL_PROGRAM_ID;
|
|
547
|
+
const mismatch = (reason, detail) => ({ status: 'mismatch', reason, detail });
|
|
548
|
+
if (!info.owner.equals(programId)) {
|
|
549
|
+
return mismatch('not_a_referral_account', `the address is in use by an account owned by ${info.owner.toBase58()}`);
|
|
550
|
+
}
|
|
551
|
+
let account;
|
|
552
|
+
try {
|
|
553
|
+
account = decodeJupiterReferralAccount(Buffer.from(info.data));
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
return mismatch('not_a_referral_account', error.message);
|
|
557
|
+
}
|
|
558
|
+
if (!account.partner.equals(expected.partner)) {
|
|
559
|
+
return mismatch('partner_mismatch', `the referral account partner is ${account.partner.toBase58()}`);
|
|
560
|
+
}
|
|
561
|
+
if (!account.project.equals(expected.project)) {
|
|
562
|
+
return mismatch('project_mismatch', `the referral account project is ${account.project.toBase58()}`);
|
|
563
|
+
}
|
|
564
|
+
if (account.name !== expected.name) {
|
|
565
|
+
return mismatch('name_mismatch', account.name === null
|
|
566
|
+
? 'the referral account has no name'
|
|
567
|
+
: `the referral account name is "${account.name}"`);
|
|
568
|
+
}
|
|
569
|
+
return { status: 'ready', account };
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* What the v2 collection account address holds: absent, a token account of `mint` owned by the
|
|
573
|
+
* referral account, or not.
|
|
574
|
+
*/
|
|
575
|
+
export function inspectJupiterReferralTokenAccountV2(address, info, expected) {
|
|
576
|
+
if (!info)
|
|
577
|
+
return { status: 'absent' };
|
|
578
|
+
const mismatch = (reason, detail) => ({ status: 'mismatch', reason, detail });
|
|
579
|
+
if (!info.owner.equals(expected.tokenProgram)) {
|
|
580
|
+
return mismatch('not_a_token_account', `the address is in use by an account owned by ${info.owner.toBase58()}`);
|
|
581
|
+
}
|
|
582
|
+
let account;
|
|
583
|
+
try {
|
|
584
|
+
account = unpackAccount(address, info, expected.tokenProgram);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return mismatch('not_a_token_account', 'the account data is not a token account');
|
|
588
|
+
}
|
|
589
|
+
if (!account.mint.equals(expected.mint)) {
|
|
590
|
+
return mismatch('wrong_mint', `the token account mint is ${account.mint.toBase58()}`);
|
|
591
|
+
}
|
|
592
|
+
if (!account.owner.equals(expected.referralAccount)) {
|
|
593
|
+
return mismatch('wrong_owner', `the token account owner is ${account.owner.toBase58()}`);
|
|
594
|
+
}
|
|
595
|
+
return { status: 'ready', amount: account.amount };
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Reads what the setup needs: the project (must exist and decode), the mints (their token
|
|
599
|
+
* program), the named referral account and its v2 collection accounts, each inspected. Mainnet
|
|
600
|
+
* only (`wrong_cluster` on any other cluster, before any read). The caller decides what to
|
|
601
|
+
* create (the absent ones) and refuses on any mismatch.
|
|
602
|
+
*/
|
|
603
|
+
export async function readJupiterReferralSetup(p) {
|
|
604
|
+
const programId = p.programId ?? JUPITER_REFERRAL_PROGRAM_ID;
|
|
605
|
+
const project = p.project ?? JUPITER_ULTRA_REFERRAL_PROJECT;
|
|
606
|
+
const name = assertJupiterReferralName(p.name);
|
|
607
|
+
const commitment = p.commitment ?? 'confirmed';
|
|
608
|
+
const retry = p.retry ?? {};
|
|
609
|
+
if (p.mints.length === 0)
|
|
610
|
+
throw new JupiterReferralError('invalid_params', 'at least one mint');
|
|
611
|
+
if (new Set(p.mints.map((m) => m.toBase58())).size !== p.mints.length) {
|
|
612
|
+
throw new JupiterReferralError('invalid_params', 'duplicate mint');
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
await assertCluster(p.connection, 'mainnet-beta', retry);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
if (error instanceof PaymentError && error.code === 'wrong_cluster') {
|
|
619
|
+
throw new JupiterReferralError('wrong_cluster', 'the Jupiter referral program and the Ultra project only exist on mainnet-beta', { cause: error });
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
const referralAccount = deriveJupiterReferralAccount(project, name, programId);
|
|
624
|
+
const [projectInfo, referralInfo, ...mintInfos] = await retryRpc(() => p.connection.getMultipleAccountsInfo([project, referralAccount, ...p.mints], commitment), retry);
|
|
625
|
+
if (!projectInfo)
|
|
626
|
+
throw new JupiterReferralError('project_not_found', 'project not found');
|
|
627
|
+
if (!projectInfo.owner.equals(programId)) {
|
|
628
|
+
throw new JupiterReferralError('not_a_project', 'project is not owned by the referral program');
|
|
629
|
+
}
|
|
630
|
+
const projectAccount = decodeJupiterProject(Buffer.from(projectInfo.data));
|
|
631
|
+
const entries = p.mints.map((mint, i) => {
|
|
632
|
+
const info = mintInfos[i];
|
|
633
|
+
if (!info) {
|
|
634
|
+
throw new JupiterReferralError('mint_not_found', `mint ${mint.toBase58()} not found`);
|
|
635
|
+
}
|
|
636
|
+
if (!info.owner.equals(TOKEN_PROGRAM_ID) && !info.owner.equals(TOKEN_2022_PROGRAM_ID)) {
|
|
637
|
+
throw new JupiterReferralError('not_a_mint', `${mint.toBase58()} is not owned by a token program`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
mint,
|
|
641
|
+
tokenProgram: info.owner,
|
|
642
|
+
tokenAccount: deriveJupiterReferralTokenAccountV2(referralAccount, mint, info.owner),
|
|
643
|
+
};
|
|
644
|
+
});
|
|
645
|
+
const tokenInfos = await retryRpc(() => p.connection.getMultipleAccountsInfo(entries.map((e) => e.tokenAccount), commitment), retry);
|
|
646
|
+
return {
|
|
647
|
+
project,
|
|
648
|
+
projectAccount,
|
|
649
|
+
name,
|
|
650
|
+
referralAccount,
|
|
651
|
+
referral: inspectJupiterReferralAccount(referralInfo ?? null, {
|
|
652
|
+
partner: p.partner,
|
|
653
|
+
project,
|
|
654
|
+
name,
|
|
655
|
+
programId,
|
|
656
|
+
}),
|
|
657
|
+
tokenAccounts: entries.map((entry, i) => ({
|
|
658
|
+
...entry,
|
|
659
|
+
status: inspectJupiterReferralTokenAccountV2(entry.tokenAccount, tokenInfos[i] ?? null, {
|
|
660
|
+
referralAccount,
|
|
661
|
+
mint: entry.mint,
|
|
662
|
+
tokenProgram: entry.tokenProgram,
|
|
663
|
+
}),
|
|
664
|
+
})),
|
|
665
|
+
};
|
|
295
666
|
}
|
|
@@ -38,6 +38,8 @@ export declare const METADATA_LIMITS: {
|
|
|
38
38
|
* buy): every byte of the three strings is one byte of the transaction, and a 32-byte name, a
|
|
39
39
|
* 10-byte symbol and a 106-byte URI land at exactly 1,232 bytes (pool.test.ts, "size"). A smaller
|
|
40
40
|
* launch (no first buy, no nonce) has more room; this is the bound for the signer's shape.
|
|
41
|
+
* A priority fee never shrinks it: up to 136 bytes the launch carries the unit limit and the
|
|
42
|
+
* price, up to 144 the price alone, beyond that no price (`priorityFeePlacement`, pool.ts).
|
|
41
43
|
*/
|
|
42
44
|
export declare const LAUNCH_METADATA_BUDGET_BYTES = 148;
|
|
43
45
|
/** Largest first buy (the spec bound, `launchpadCoin.firstBuySol` <= 10 SOL). */
|
package/dist/launch-metadata.js
CHANGED
|
@@ -25,6 +25,8 @@ export const METADATA_LIMITS = { name: 32, symbol: 10, uri: 200 };
|
|
|
25
25
|
* buy): every byte of the three strings is one byte of the transaction, and a 32-byte name, a
|
|
26
26
|
* 10-byte symbol and a 106-byte URI land at exactly 1,232 bytes (pool.test.ts, "size"). A smaller
|
|
27
27
|
* launch (no first buy, no nonce) has more room; this is the bound for the signer's shape.
|
|
28
|
+
* A priority fee never shrinks it: up to 136 bytes the launch carries the unit limit and the
|
|
29
|
+
* price, up to 144 the price alone, beyond that no price (`priorityFeePlacement`, pool.ts).
|
|
28
30
|
*/
|
|
29
31
|
export const LAUNCH_METADATA_BUDGET_BYTES = 148;
|
|
30
32
|
/** Largest first buy (the spec bound, `launchpadCoin.firstBuySol` <= 10 SOL). */
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { PublicKey, Transaction, TransactionInstruction, type AccountInfo, type Commitment, type Connection } from '@solana/web3.js';
|
|
2
|
+
import { type PriorityFeeQuote } from './priority-fee.js';
|
|
3
|
+
import { type RetryOptions } from './reads.js';
|
|
4
|
+
/** Default seed of the referral account address (`createWithSeed(payer, seed, SPL Token)`). */
|
|
5
|
+
export declare const METEORA_REFERRAL_ACCOUNT_SEED = "forge-meteora-referral";
|
|
6
|
+
/** Longest seed `createWithSeed` accepts, in bytes (System program limit). */
|
|
7
|
+
export declare const MAX_METEORA_REFERRAL_SEED_BYTES = 32;
|
|
8
|
+
/**
|
|
9
|
+
* Compute unit limit of the setup transaction with a priority fee: one `createAccountWithSeed`
|
|
10
|
+
* and one `InitializeAccount3` (a few thousand units each), with a wide margin.
|
|
11
|
+
*/
|
|
12
|
+
export declare const METEORA_REFERRAL_SETUP_COMPUTE_UNITS = 30000;
|
|
13
|
+
/** SPL Token `InitializeAccount3` instruction tag (u8), followed by the 32-byte owner. */
|
|
14
|
+
export declare const TOKEN_INITIALIZE_ACCOUNT3_TAG = 18;
|
|
15
|
+
export type MeteoraReferralErrorCode = 'invalid_params';
|
|
16
|
+
export declare class MeteoraReferralError extends Error {
|
|
17
|
+
readonly code: MeteoraReferralErrorCode;
|
|
18
|
+
constructor(code: MeteoraReferralErrorCode, message: string, options?: {
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Checks a `createWithSeed` seed: 1 to 32 UTF-8 bytes, no control character. */
|
|
23
|
+
export declare function assertMeteoraReferralSeed(seed: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Address of the referral account created by `buildCreateMeteoraReferralAccountTx` for `payer`
|
|
26
|
+
* and `seed`: `createWithSeed(payer, seed, SPL Token program)`, i.e.
|
|
27
|
+
* `sha256(payer || seed || program)`, the same derivation as `PublicKey.createWithSeed`
|
|
28
|
+
* (synchronous here, so the builders stay pure).
|
|
29
|
+
*/
|
|
30
|
+
export declare function deriveMeteoraReferralAccount(payer: PublicKey, seed?: string): PublicKey;
|
|
31
|
+
export interface CreateMeteoraReferralAccountParams {
|
|
32
|
+
/** Fee payer, rent payer and base of the address derivation: the only signer. */
|
|
33
|
+
payer: PublicKey;
|
|
34
|
+
/** Owner of the token account: the FORGE revenue wallet (D34). */
|
|
35
|
+
owner: PublicKey;
|
|
36
|
+
/** Seed of the address (default `METEORA_REFERRAL_ACCOUNT_SEED`), 1 to 32 bytes. */
|
|
37
|
+
seed?: string;
|
|
38
|
+
/** Rent-exempt minimum of a token account (`ACCOUNT_SIZE` = 165 bytes), read by the caller. */
|
|
39
|
+
rentLamports: bigint;
|
|
40
|
+
}
|
|
41
|
+
export interface MeteoraReferralAccountInstructions {
|
|
42
|
+
/** The new token account: `deriveMeteoraReferralAccount(payer, seed)`. */
|
|
43
|
+
account: PublicKey;
|
|
44
|
+
seed: string;
|
|
45
|
+
/** `createAccountWithSeed` (payer -> account, rent, 165 bytes, SPL Token) then `InitializeAccount3`. */
|
|
46
|
+
instructions: TransactionInstruction[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The two instructions creating the standalone wrapped-SOL token account of `owner`:
|
|
50
|
+
* System `createAccountWithSeed` (from = base = payer; exactly the rent of 165 bytes; owner
|
|
51
|
+
* program = SPL Token) and SPL Token `InitializeAccount3` (account, wrapped SOL mint; owner in
|
|
52
|
+
* the data, no owner signature). Refuses an owner equal to the payer, a zero rent, and a seed
|
|
53
|
+
* whose address is the owner's associated wrapped-SOL account (impossible in practice: the
|
|
54
|
+
* associated account is a PDA, this address is a hash; checked all the same).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildCreateMeteoraReferralAccountIxs(p: CreateMeteoraReferralAccountParams): MeteoraReferralAccountInstructions;
|
|
57
|
+
export interface BuildCreateMeteoraReferralAccountTxParams extends CreateMeteoraReferralAccountParams {
|
|
58
|
+
blockhash: string;
|
|
59
|
+
lastValidBlockHeight: number;
|
|
60
|
+
/**
|
|
61
|
+
* Priority fee quote of the caller (`METEORA_REFERRAL_SETUP_COMPUTE_UNITS`): a unit limit then
|
|
62
|
+
* a unit price are put first when its price is not 0; nothing otherwise.
|
|
63
|
+
*/
|
|
64
|
+
priorityFee?: Pick<PriorityFeeQuote, 'computeUnitLimit' | 'microLamports'>;
|
|
65
|
+
}
|
|
66
|
+
export interface CreateMeteoraReferralAccountTx extends MeteoraReferralAccountInstructions {
|
|
67
|
+
/** Unsigned legacy transaction (fee payer = payer, blockhash set). */
|
|
68
|
+
transaction: Transaction;
|
|
69
|
+
lastValidBlockHeight: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Unsigned transaction creating the Meteora referral account (`buildCreateMeteoraReferralAccountIxs`
|
|
73
|
+
* after the optional compute budget). It does not check that the address is free: inspect it
|
|
74
|
+
* first (`readMeteoraReferralAccount`); creating it twice fails on-chain, nothing is lost but
|
|
75
|
+
* the fee.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildCreateMeteoraReferralAccountTx(p: BuildCreateMeteoraReferralAccountTxParams): CreateMeteoraReferralAccountTx;
|
|
78
|
+
export type MeteoraReferralMismatch =
|
|
79
|
+
/** The address holds something that is not an SPL Token account (or none that decodes). */
|
|
80
|
+
'not_a_token_account' | 'wrong_mint' | 'wrong_owner'
|
|
81
|
+
/** The account is the owner's associated wrapped-SOL account: Jupiter claims would land in it. */
|
|
82
|
+
| 'associated_account' | 'frozen'
|
|
83
|
+
/** A delegate or a close authority could move or close it: not acceptable for a fee account. */
|
|
84
|
+
| 'delegated' | 'close_authority';
|
|
85
|
+
export type MeteoraReferralAccountStatus =
|
|
86
|
+
/** No account at the address: the setup creates it. */
|
|
87
|
+
{
|
|
88
|
+
status: 'absent';
|
|
89
|
+
}
|
|
90
|
+
/** A wrapped-SOL token account held by the expected owner, standalone: nothing to do. */
|
|
91
|
+
| {
|
|
92
|
+
status: 'ready';
|
|
93
|
+
lamports: bigint;
|
|
94
|
+
amount: bigint;
|
|
95
|
+
}
|
|
96
|
+
/** Something else lives there: never create, never reuse (the caller refuses). */
|
|
97
|
+
| {
|
|
98
|
+
status: 'mismatch';
|
|
99
|
+
reason: MeteoraReferralMismatch;
|
|
100
|
+
detail: string;
|
|
101
|
+
};
|
|
102
|
+
export interface MeteoraReferralExpectation {
|
|
103
|
+
/** The FORGE revenue wallet. */
|
|
104
|
+
owner: PublicKey;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What `address` holds, against the expected shape of the Meteora referral account: absent,
|
|
108
|
+
* ready (a standalone wrapped-SOL token account of `owner`, neither frozen, delegated nor
|
|
109
|
+
* closable by someone else), or a mismatch with its reason.
|
|
110
|
+
*/
|
|
111
|
+
export declare function inspectMeteoraReferralAccount(address: PublicKey, info: AccountInfo<Buffer> | null, expected: MeteoraReferralExpectation): MeteoraReferralAccountStatus;
|
|
112
|
+
export interface ReadMeteoraReferralAccountParams extends MeteoraReferralExpectation {
|
|
113
|
+
connection: Pick<Connection, 'getAccountInfo'>;
|
|
114
|
+
address: PublicKey;
|
|
115
|
+
commitment?: Commitment;
|
|
116
|
+
retry?: RetryOptions;
|
|
117
|
+
}
|
|
118
|
+
/** Reads `address` and inspects it (`inspectMeteoraReferralAccount`). Any cluster. */
|
|
119
|
+
export declare function readMeteoraReferralAccount(p: ReadMeteoraReferralAccountParams): Promise<MeteoraReferralAccountStatus>;
|