@forgedevv/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 30 bps integrator fee is collected
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 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.
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,21 +17,43 @@
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
- * 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.
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
40
  import { createHash } from 'node:crypto';
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
58
  return createHash('sha256').update(name).digest().subarray(0, 8);
36
59
  }
@@ -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). */
@@ -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>;