@gabox-labs/sdk 0.2.0 → 0.6.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/dist/index.js CHANGED
@@ -1,27 +1,69 @@
1
- import { $t as findPoolPda, Dn as decodePool, En as POOL_DISCRIMINATOR, Hn as fetchMaybeDraw, In as getPoolSize, Kt as getBindReferrerInstructionAsync, L as getInitializePoolInstructionAsync, Ln as DRAW_DISCRIMINATOR, Lt as getBuyPackInstruction, O as getRetryDrawInstruction, Qt as findReferralPda, Rn as decodeDraw, St as getClaimReferralInstructionAsync, W as getFundPrizesInstruction, Zt as findReferralLinkPda, an as decodeReferralLink, at as getExpireDrawInstruction, b as getSellPrizeInstruction, cn as fetchMaybeReferralLink, f as getSellTokensInstruction, in as REFERRAL_LINK_DISCRIMINATOR, kt as getClaimPrizeInstruction, mn as getReferralLinkSize, nn as findDrawPda, tn as findIdentityPda, yn as fetchMaybeReferral } from "./gaboxV2-CV2XltqC.js";
2
- import { $ as MAX_ATTEMPTS, Q as INSTRUCTIONS_SYSVAR, T as PUMP_GLOBAL, U as decodePumpGlobal, X as GABOX_PROGRAM_ID, Z as IDENTITY_SEED, at as RETRY_SLOTS, ct as TOKEN_2022_PROGRAM_ADDRESS, d as pumpBuyAccounts, dt as VRF_PROGRAM_ADDRESS, et as MAX_FEE_BPS, ft as WSOL_MINT, gt as PUMP_CREATE_TOKEN_PROGRAM_ADDRESS, h as swapCashbackAccount, i as getPumpCreateV2Instruction, it as REFERRAL_FEE_BPS, l as tokenAccountAmount, lt as TOKEN_PROGRAM_ADDRESS, mt as MAYHEM_PROGRAM_ADDRESS, n as venueBuyInstruction, nt as PROTOCOL_FEE_BPS, o as newCurveBuyCost, ot as SLOT_HASHES_SYSVAR, pt as ASSOCIATED_TOKEN_PROGRAM_ADDRESS, rt as PROTOCOL_FEE_COLLECTOR, s as resolveVenue, st as TIMEOUT_SLOTS, t as pump_exports, tt as PACK_TOKENS, u as wsolAccountFor, ut as VRF_DEFAULT_QUEUE, vt as PUMP_FEE_PROGRAM_ADDRESS, wt as SYSTEM_PROGRAM_ADDRESS, xt as PUMP_SWAP_PROGRAM_ADDRESS, yt as PUMP_PROGRAM_ADDRESS } from "./pump-D0K_0uiC.js";
3
- import { DRAW_RESOLVED_EVENT_DISCRIMINATOR, PACK_BOUGHT_EVENT_DISCRIMINATOR, POOL_CREATED_EVENT_DISCRIMINATOR, PRIZES_FUNDED_EVENT_DISCRIMINATOR, PRIZE_REDEEMED_EVENT_DISCRIMINATOR, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR, TOKENS_SOLD_EVENT_DISCRIMINATOR, getDrawResolvedEventDecoder, getPackBoughtEventDecoder, getPoolCreatedEventDecoder, getPrizeRedeemedEventDecoder, getPrizesFundedEventDecoder, getRandomnessRetriedEventDecoder, getTokensSoldEventDecoder, t as generated_exports } from "./generated/index.js";
4
- import { address, appendTransactionMessageInstructions, compileTransaction, compressTransactionMessageUsingAddressLookupTables, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, fetchEncodedAccount, fetchEncodedAccounts, getAddressEncoder, getBase58Decoder, getBase64Encoder, getBytesEncoder, getProgramDerivedAddress, getStructEncoder, getTransactionEncoder, getU32Encoder, getU64Encoder, getU8Encoder, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
5
- import { TOKEN_PROGRAM_ADDRESS as TOKEN_PROGRAM_ADDRESS$1, getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
1
+ import { B as COMPUTE_BUDGET_PROGRAM_ADDRESS, E as curveBuyExactOut, G as computeBudgetInstructions, H as DEFAULT_COMPUTE_UNIT_LIMIT, K as getSetComputeUnitLimitInstruction, Lt as raydiumIds, P as launchlabBuyAccounts, R as BUY_PACK_COMPUTE_UNITS, U as MAX_COMPUTE_UNIT_LIMIT, V as CREATE_MACHINE_COMPUTE_UNITS, W as REDEEM_COMPUTE_UNITS, _ as newCurveReserves, b as wsolAccountFor, c as getLaunchInstruction, cn as WSOL_MINT, ct as ata, en as LAUNCH_TOTAL_BASE_SELL, ft as creatorFeeVaultAddress, kt as LAUNCH_DECIMALS, l as buildMessage, m as fetchCurveSettings, nn as PLATFORM_ADMIN, on as SYSTEM_PROGRAM_ADDRESS, ot as tokenAccountAmount, q as getSetComputeUnitPriceInstruction, sn as TOKEN_PROGRAM_ADDRESS, t as raydium_exports, tn as METAPLEX_PROGRAM_ADDRESS, u as withRemainingAccounts, v as resolveVenue, vt as launchlabPoolAddress, xt as platformFeeVaultAddress, yt as launchlabVaultAddress, z as CLAIM_COMPUTE_UNITS, zt as ASSOCIATED_TOKEN_PROGRAM_ADDRESS } from "./raydium-B-l9V3O-.js";
2
+ import { DRAW_DISCRIMINATOR, DRAW_RESOLVED_EVENT_DISCRIMINATOR, GABOX_PROGRAM_ADDRESS, PACK_BOUGHT_EVENT_DISCRIMINATOR, POOL_CREATED_EVENT_DISCRIMINATOR, POOL_DISCRIMINATOR, PRIZES_FUNDED_EVENT_DISCRIMINATOR, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR, TOKENS_SOLD_EVENT_DISCRIMINATOR, WALLET_ACTIVITY_DISCRIMINATOR, decodeDraw, decodePool, decodeWalletActivity, findActivityPda, findDrawPda, findIdentityPda, findPoolPda, getBuyPackInstructionAsync, getDrawResolvedEventDecoder, getDrawSize, getExpireDrawInstruction, getFundPrizesInstruction, getInitializePoolInstructionAsync, getPackBoughtEventDecoder, getPoolCreatedEventDecoder, getPoolSize, getPrizesFundedEventDecoder, getRandomnessRetriedEventDecoder, getRetryDrawInstruction, getSellTokensInstructionAsync, getTokensSoldEventDecoder, getWalletActivitySize, t as generated_exports } from "./generated/index.js";
3
+ import { address, createSolanaRpc, createSolanaRpcSubscriptions, fetchEncodedAccount, getAddressEncoder, getBase58Decoder, getBase64Encoder, getBytesEncoder, getProgramDerivedAddress } from "@solana/kit";
4
+ import { getCloseAccountInstruction, getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
6
5
  import { getTransferSolInstruction } from "@solana-program/system";
6
+ //#region src/ids.ts
7
+ /** `declare_id!` in `programs/gabox/src/lib.rs`. */
8
+ const GABOX_PROGRAM_ID = GABOX_PROGRAM_ADDRESS;
9
+ /** MagicBlock's ephemeral VRF program. `vrf.rs` pins it and refuses any other. */
10
+ const VRF_PROGRAM_ADDRESS = "Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz";
11
+ /**
12
+ * MagicBlock's default oracle queue. The program pins this one address, so a pool creator cannot
13
+ * point a draw at an oracle they control.
14
+ */
15
+ const VRF_DEFAULT_QUEUE = "Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh";
16
+ /** The seed of both identity PDAs. See `pdas.ts` — there are two, under different programs. */
17
+ const IDENTITY_SEED = new Uint8Array([
18
+ 105,
19
+ 100,
20
+ 101,
21
+ 110,
22
+ 116,
23
+ 105,
24
+ 116,
25
+ 121
26
+ ]);
27
+ const SLOT_HASHES_SYSVAR = "SysvarS1otHashes111111111111111111111111111";
28
+ const INSTRUCTIONS_SYSVAR = "Sysvar1nstructions1111111111111111111111111";
29
+ /** `constants.rs`. A retry may not run sooner than this after the last attempt. */
30
+ const RETRY_SLOTS = 300n;
31
+ /** `constants.rs`. After this many slots from the purchase, anyone may expire the draw. */
32
+ const TIMEOUT_SLOTS = 216000n;
33
+ /** `constants.rs`. Three requests in total, counting the one `buy_pack` makes. */
34
+ const MAX_ATTEMPTS = 3;
35
+ /**
36
+ * `constants.rs`. Tokens in one pack, in base units: 1,000,000 tokens at the fixed 6 decimals, or
37
+ * 0.1% of the 1,000,000,000 supply. Every pool sells packs of this size. The venue decides what a
38
+ * pack costs, so the pack price follows the coin. Read `pool.packTokens` rather than this when a
39
+ * pool is at hand: a later program version may change the constant.
40
+ */
41
+ const PACK_TOKENS = 1000000n * 1000000n;
42
+ //#endregion
7
43
  //#region src/math.ts
8
44
  /**
9
- * A bigint port of `programs/gabox-v2/src/math.rs`.
45
+ * A bigint port of `programs/gabox/src/math.rs`.
10
46
  *
11
47
  * The point is that a client can show a buyer the exact prize table the program will freeze into
12
48
  * their `Draw`, before they pay. Every rounding step here matches the Rust, including the direction
13
- * of each division. `test/math.test.ts` runs the same vectors as `programs/gabox-v2/tests/
14
- * economics.rs`.
49
+ * of each division. `test/math.test.ts` runs the same vectors as `programs/gabox/tests/math.rs`.
15
50
  *
16
51
  * All amounts are `bigint`, in the mint's smallest unit. `multiplierBps` and `tickets` are numbers
17
52
  * because both are `u32` in the program and both stay small.
18
53
  */
19
54
  /** Basis points. A multiplier of 10_000 pays back exactly one pack. */
20
55
  const BPS = 10000n;
21
- /** `math::share`. `bps` basis points of `amount`, rounded down. */
22
- function share(amount, bps) {
23
- return amount * bps / BPS;
24
- }
56
+ /**
57
+ * `constants::MIN_SEED_MULTIPLIER_BPS`. The mandatory seed makes a 3x top prize payable on the
58
+ * first pack, or the full top prize when the table's top tier is below 3x. A creator can add more
59
+ * seed on top through `extraSeedTokens`.
60
+ */
61
+ const MIN_SEED_MULTIPLIER_BPS = 3e4;
62
+ /**
63
+ * `constants::MAX_MULTIPLIER_BPS`. A table's top tier can be at most 20x. Seed beyond what a 20x
64
+ * prize needs stays in the vault as backup for the draws after a top-tier hit.
65
+ */
66
+ const MAX_MULTIPLIER_BPS = 2e5;
25
67
  /** Ticket counts must sum to this. A uniform 16-bit word then maps with no modulo bias. */
26
68
  const TICKETS = 65536;
27
69
  /** The prize table has exactly this many slots. Unused slots are all-zero. */
@@ -95,9 +137,10 @@ function tierAmount(base, multiplierBps) {
95
137
  /**
96
138
  * `math::validate`. Checks the table alone, with no base.
97
139
  *
98
- * Three rules, and each one closes a way to sell a bad ticket:
140
+ * Four rules, and each one closes a way to sell a bad ticket:
99
141
  * - ticket counts sum to exactly 65,536, so the 16-bit draw is uniform;
100
142
  * - an unused row is all-zero, so a hidden multiplier cannot ride along;
143
+ * - a ticketed tier's multiplier is from 1 to `MAX_MULTIPLIER_BPS` (20x);
101
144
  * - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
102
145
  * tokens than a pack buys. This bounds tokens, not cash value.
103
146
  */
@@ -113,6 +156,7 @@ function validateTiers(tiers) {
113
156
  continue;
114
157
  }
115
158
  if (tier.multiplierBps <= 0) throw new GaboxMathError("InvalidDistribution", "a ticketed tier must pay something; it would sell a ticket that can only win zero");
159
+ if (tier.multiplierBps > 2e5) throw new GaboxMathError("InvalidDistribution", `tier ${index} multiplierBps exceeds the ${MAX_MULTIPLIER_BPS} maximum (20x)`);
116
160
  count += BigInt(tier.tickets);
117
161
  expected += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
118
162
  }
@@ -135,24 +179,30 @@ function validatePack(packTokens, tiers) {
135
179
  }
136
180
  }
137
181
  /**
138
- * `math::seed_tokens`. The seed a table needs, in tokens.
182
+ * `math::seed_tokens`. The mandatory seed a table needs, in tokens.
139
183
  *
140
- * The first pack must be able to pay the largest tier in full. The pack itself brings `packTokens`
141
- * into the vault, so the vault has to hold the rest beforehand:
184
+ * The mandatory seed makes a 3x top prize payable on the first pack, or the full top prize when
185
+ * the table's top tier is below 3x. A jackpot above 3x still shows the full table once later
186
+ * packs grow the free vault; the mandatory seed alone does not have to fund it. The pack itself
187
+ * brings `packTokens` into the vault, so the vault has to hold the rest beforehand:
142
188
  *
143
- * seedTokens = uncappedMaximum(packTokens, tiers) - packTokens
189
+ * seedTokens = min(uncappedMaximum(packTokens, tiers), tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS)) - packTokens
144
190
  *
145
- * A 5x jackpot on a 1M-token pack needs a 4M-token seed. A table whose top tier pays exactly one
146
- * pack needs no seed. A top tier below one pack is refused: every ticket would lose.
191
+ * A 20x or a 3x top tier on a 1M-token pack both need a 2M-token seed, since the mandatory seed
192
+ * stops at the 3x minimum. A 2x top tier needs a 1M-token seed, already below the minimum. A
193
+ * table whose top tier pays exactly one pack needs no seed. A top tier below one pack is refused:
194
+ * every ticket would lose. `createMachine` lets a creator add more seed on top of this mandatory
195
+ * amount through `extraSeedTokens`, for example to fund a top tier above 3x from the first pack.
147
196
  *
148
- * `initialize_pool` computes this number itself and buys exactly that many tokens on the curve.
197
+ * Pool initialization computes this number itself and buys exactly that many tokens on the curve.
149
198
  * The creator only signs a maximum SOL cost.
150
199
  */
151
200
  function seedTokens(packTokens, tiers) {
152
201
  validateTiers(tiers);
153
202
  const largest = uncappedMaximum(packTokens, tiers);
154
203
  if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
155
- return largest - packTokens;
204
+ const minimum = tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS);
205
+ return (largest < minimum ? largest : minimum) - packTokens;
156
206
  }
157
207
  /**
158
208
  * `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
@@ -256,589 +306,284 @@ function averageMultiplierBps(tiers) {
256
306
  //#endregion
257
307
  //#region src/pdas.ts
258
308
  /**
259
- * Every program-derived address a client needs.
309
+ * Gabox's own program-derived addresses.
260
310
  *
261
- * The three gabox PDAs come from the generated tree, which Codama built from the IDL's own seed
262
- * metadata. Re-exported here so callers have one import, and so the seeds stay in exactly one
263
- * place. The two written by hand are the ones Codama cannot generate: an ATA, and a PDA that lives
264
- * under a program other than ours.
265
- */
266
- /** `['referral', referee]`. A wallet's permanent referral binding. */
267
- async function referralLinkAddress(referee) {
268
- return (await findReferralLinkPda({ purchaser: referee }))[0];
269
- }
270
- /** `['referral', pool, referrer]`. A referrer's accrued rewards for one pool. */
271
- async function referralAddress(pool, referrer) {
272
- return (await findReferralPda({
273
- pool,
274
- referralLink: referrer
275
- }))[0];
276
- }
277
- /** `["pool", mint]`. One pool per coin, and the mint alone is the seed. */
278
- async function poolAddress(mint) {
279
- return (await findPoolPda({ mint }))[0];
280
- }
281
- /** `["draw", pool, seq_u64_le]`. `seq` is `pool.nextSeq` at the moment of the purchase. */
282
- async function drawAddress(pool, seq) {
283
- return (await findDrawPda({
284
- pool,
285
- seq
286
- }))[0];
287
- }
288
- /**
289
- * `["identity"]` under gabox. This is the PDA gabox signs the VRF request with — the `identity`
290
- * account of the `Oracle` group on `buy_pack` and `retry_draw`.
291
- */
292
- async function vrfIdentityAddress() {
293
- return (await findIdentityPda())[0];
294
- }
295
- /**
296
- * `["identity", gabox_program_id]` under the **VRF program**. A different address from
297
- * `vrfIdentityAddress`, and it belongs to the other side: MagicBlock signs the `deliver_draw`
298
- * callback with it. A client never puts it in an instruction. It is here so a client can recognise
299
- * the signer on a callback transaction.
311
+ * Raydium's live in `src/raydium/pdas.ts`, because they depend on the cluster. These do not: the
312
+ * Gabox program id is the same everywhere.
313
+ *
314
+ * Classic SPL Token only. Both venues use it, and the accepted LaunchLab create instruction mints
315
+ * the coin with it, so no derivation here takes a token program any more.
300
316
  */
317
+ /** `["pool", mint]`. One pool per coin. */
318
+ const poolAddress = async (mint) => (await findPoolPda({ mint }))[0];
319
+ /** `["draw", pool, seq]`, with the sequence as eight little-endian bytes. */
320
+ const drawAddress = async (pool, seq) => (await findDrawPda({
321
+ pool,
322
+ seq
323
+ }))[0];
324
+ /** The per-wallet `WalletActivity` PDA. `buy_pack` derives it from the purchaser automatically. */
325
+ const activityAddress = async (wallet) => (await findActivityPda({ purchaser: wallet }))[0];
326
+ /** `["identity"]` under Gabox. The PDA the program signs its randomness request with. */
327
+ const vrfIdentityAddress = async () => (await findIdentityPda())[0];
328
+ /** `["identity", gabox_program_id]` under the VRF program. MagicBlock signs the callback with it. */
301
329
  async function scopedVrfIdentityAddress() {
302
330
  return await getProgramDerivedAddress({
303
331
  programAddress: VRF_PROGRAM_ADDRESS,
304
332
  seeds: [getBytesEncoder().encode(IDENTITY_SEED), getAddressEncoder().encode(GABOX_PROGRAM_ID)]
305
333
  });
306
334
  }
307
- /**
308
- * An associated token account. The seed order is `[owner, token_program, mint]`, which is the ATA
309
- * program's own order and not the argument order most callers remember.
310
- */
311
- async function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
312
- const encoder = getAddressEncoder();
313
- const [address] = await getProgramDerivedAddress({
314
- programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
315
- seeds: [
316
- encoder.encode(owner),
317
- encoder.encode(tokenProgram),
318
- encoder.encode(mint)
319
- ]
320
- });
321
- return address;
322
- }
323
- /**
324
- * The protocol fee collector's WSOL ATA, under classic SPL Token. A sale on PumpSwap pays its
325
- * protocol fee here. The account must exist before the first PumpSwap sale; see `DEPLOYMENT.md`.
326
- */
327
- async function feeCollectorWsolAddress() {
328
- return await associatedTokenAddress(PROTOCOL_FEE_COLLECTOR, WSOL_MINT, TOKEN_PROGRAM_ADDRESS);
329
- }
330
- /**
331
- * The pool's prize inventory: the pool PDA's ATA for the mint's own token program.
332
- *
333
- * `tokenProgram` defaults to Token-2022 because Pump `create_v2` mints there, and every machine is
334
- * built on a coin Pump created. Pass the mint's real owner if you have it.
335
- */
336
- async function vaultAddress(mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
337
- return await associatedTokenAddress(await poolAddress(mint), mint, tokenProgram);
338
- }
335
+ /** An associated token account under classic SPL Token. */
336
+ const associatedTokenAddress = async (owner, mint, tokenProgram = TOKEN_PROGRAM_ADDRESS) => await ata(owner, mint, tokenProgram);
337
+ /** The prize vault: the pool PDA's own associated token account for the coin. */
338
+ const vaultAddress = async (mint) => await associatedTokenAddress(await poolAddress(mint), mint);
339
339
  //#endregion
340
340
  //#region src/accounts.ts
341
- /**
342
- * Reading gabox state: pools, draws, and the vault balance that turns a pool into an offer.
343
- *
344
- * The decoders are the generated ones. What this file adds is the queries — the discriminator and
345
- * `memcmp` filters that let a client list every machine, or every draw a wallet is waiting on,
346
- * without an indexer.
347
- *
348
- * # The offsets are computed, not counted
349
- *
350
- * A `memcmp` filter is a byte offset into an account. Counting field widths by hand is how a filter
351
- * silently matches nothing. Each offset below is built from the widths of the fields before it, in
352
- * the same order `state.rs` declares them, so the arithmetic is visible and a field inserted in the
353
- * middle changes it.
354
- */
341
+ /** Account readers and bounded discovery scans. Old pool layouts are intentionally unsupported. */
355
342
  const DISCRIMINATOR = 8;
356
- /** `Draw.pool` sits straight after the discriminator. */
357
343
  const DRAW_POOL_OFFSET = DISCRIMINATOR;
358
- /** `Draw.purchaser` sits after the discriminator and `pool`. */
359
344
  const DRAW_PURCHASER_OFFSET = 40;
360
- /** `Pool.creator` sits straight after the discriminator. */
361
345
  const POOL_CREATOR_OFFSET = DISCRIMINATOR;
362
- /** `Pool.mint` sits after the discriminator and `creator`. */
363
346
  const POOL_MINT_OFFSET = 40;
364
- /** `ReferralLink.referrer` sits after the discriminator and `referee`. */
365
- const REFERRAL_LINK_REFERRER_OFFSET = 40;
366
347
  const base58 = getBase58Decoder();
367
- const base64$1 = getBase64Encoder();
368
- /** A `memcmp` filter wants base58, and both discriminators and addresses arrive as other things. */
348
+ const base64 = getBase64Encoder();
369
349
  const asBase58 = (bytes) => base58.decode(bytes);
370
350
  const memcmp = (offset, bytes) => ({ memcmp: {
371
351
  offset: BigInt(offset),
372
352
  bytes,
373
353
  encoding: "base58"
374
354
  } });
375
- async function scan(rpc, filters, decode) {
376
- return (await rpc.getProgramAccounts(GABOX_PROGRAM_ID, {
377
- encoding: "base64",
378
- commitment: "confirmed",
379
- filters
380
- }).send()).map(({ pubkey, account }) => decode({
381
- address: pubkey,
382
- data: new Uint8Array(base64$1.encode(account.data[0]))
383
- }));
384
- }
385
- /** Wrap raw bytes in the shape the generated decoders expect. */
386
355
  const encoded = (address, data) => ({
387
356
  address,
388
357
  data,
358
+ exists: true,
389
359
  executable: false,
390
360
  lamports: 0n,
391
361
  programAddress: GABOX_PROGRAM_ID,
392
362
  space: BigInt(data.length)
393
363
  });
394
- /** The pool for a coin, or `null` when the coin has no machine. */
395
364
  async function fetchPoolByMint(client, mint) {
396
365
  return await fetchPoolAt(client, await poolAddress(mint));
397
366
  }
398
- /**
399
- * A pool by its own address, or `null`.
400
- *
401
- * `fetchPoolByMint` is the usual way in, because a coin's mint is the natural key. This one is for
402
- * the other direction: a `Draw` names its pool and not its mint, so a client holding a draw reads
403
- * the pool to learn which coin it belongs to.
404
- */
405
367
  async function fetchPoolAt(client, address) {
406
368
  return decodeCurrentPool(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
407
369
  }
408
- /** `null` for a missing pool; an error for one on the older devnet layout. */
409
370
  function decodeCurrentPool(account) {
410
371
  if (!account.exists) return null;
411
- if (account.data.length !== getPoolSize()) throw new Error(`Machine ${account.address} uses an older devnet account layout and needs migration or recreation.`);
372
+ if (account.data.length !== getPoolSize()) throw new Error(`pool ${account.address} is not a Gabox pool and is intentionally unsupported`);
412
373
  return decodePool(account).data;
413
374
  }
414
- /** A draw by address, or `null`. Delivered and legacy-claimed draws are closed. */
375
+ function decodeCurrentDraw(account) {
376
+ if (!account.exists) return null;
377
+ if (account.data.length !== getDrawSize()) throw new Error(`draw ${account.address} is not a Gabox draw and is intentionally unsupported`);
378
+ return decodeDraw(account).data;
379
+ }
415
380
  async function fetchDraw(client, address) {
416
- const account = await fetchMaybeDraw(client.rpc, address, { commitment: "confirmed" });
417
- return account.exists ? account.data : null;
381
+ return decodeCurrentDraw(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
418
382
  }
419
- /** Accrued referral reward for one referrer and pool, or zero when no referred pack has settled. */
420
- async function fetchReferralReward(client, pool, referrer) {
421
- const address = await referralAddress(pool, referrer);
422
- const account = await fetchMaybeReferral(client.rpc, address, { commitment: "confirmed" });
423
- return account.exists ? account.data : null;
383
+ function decodeCurrentWalletActivity(account) {
384
+ if (!account.exists) return null;
385
+ if (account.data.length !== getWalletActivitySize()) throw new Error(`activity account ${account.address} is not a Gabox WalletActivity and is intentionally unsupported`);
386
+ return decodeWalletActivity(account).data;
424
387
  }
425
388
  /**
426
- * Every current-layout machine, by discriminator and account size. Older devnet
427
- * pools predate seed fields and must not be decoded with the current schema.
428
- *
429
- * There is no on-chain registry — the design says so on purpose — so discovery is this scan plus
430
- * the `PoolCreated` event. Public RPCs limit `getProgramAccounts`, so an app that lists machines
431
- * for users should cache the result rather than call this per page view.
389
+ * A wallet's lifetime pack-buying activity, or `null` before it has bought its first pack.
390
+ * `buy_pack` creates this account `init_if_needed`, so a fresh wallet has none yet.
432
391
  */
392
+ async function fetchWalletActivity(client, wallet) {
393
+ return decodeCurrentWalletActivity(await fetchEncodedAccount(client.rpc, await activityAddress(wallet), { commitment: "confirmed" }));
394
+ }
395
+ async function scan(rpc, filters, decode) {
396
+ return (await rpc.getProgramAccounts(GABOX_PROGRAM_ID, {
397
+ encoding: "base64",
398
+ commitment: "confirmed",
399
+ filters
400
+ }).send()).map(({ pubkey, account }) => decode(pubkey, new Uint8Array(base64.encode(account.data[0]))));
401
+ }
433
402
  async function listPools(client) {
434
- return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], ({ address, data }) => ({
403
+ return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], (address, data) => ({
435
404
  address,
436
405
  data: decodePool(encoded(address, data)).data
437
406
  }));
438
407
  }
439
- /** Every draw of one pool, pending or ready. Closed draws are gone and never appear. */
440
- async function listDrawsByPool(client, pool) {
441
- return await listDraws(client, { pool });
442
- }
443
- /**
444
- * Every open draw of one wallet, across all pools. This is the "what am I owed" query.
445
- *
446
- * An address is already base58, so it goes into the filter unchanged.
447
- */
448
- async function listDrawsByPurchaser(client, purchaser) {
449
- return await listDraws(client, { purchaser });
450
- }
451
- /**
452
- * Open draws, narrowed by pool, by purchaser, or by both.
453
- *
454
- * Both filters in one scan, because a UI asks for exactly that: "my pulls on this machine". Two
455
- * separate scans and an intersection in the client would move twice the bytes and could disagree
456
- * with itself, since the two reads happen at different slots. With neither filter this lists every
457
- * open draw of every machine.
458
- */
459
408
  async function listDraws(client, query = {}) {
460
- const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR))];
409
+ const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR)), { dataSize: BigInt(getDrawSize()) }];
461
410
  if (query.pool) filters.push(memcmp(8, query.pool));
462
411
  if (query.purchaser) filters.push(memcmp(40, query.purchaser));
463
- return await scan(client.rpc, filters, ({ address, data }) => ({
412
+ return await scan(client.rpc, filters, (address, data) => ({
464
413
  address,
465
414
  data: decodeDraw(encoded(address, data)).data
466
415
  }));
467
416
  }
468
- /**
469
- * Every wallet bound to one referrer. This is the "who did I refer" query.
470
- *
471
- * A scan, because a link is seeded on the referee and nothing on chain indexes it by referrer.
472
- * The list is small in practice, and a dashboard reads it once per load, not per poll.
473
- */
474
- async function listReferralLinksByReferrer(client, referrer) {
475
- return await scan(client.rpc, [
476
- memcmp(0, asBase58(REFERRAL_LINK_DISCRIMINATOR)),
477
- { dataSize: BigInt(getReferralLinkSize()) },
478
- memcmp(40, referrer)
479
- ], ({ address, data }) => ({
480
- address,
481
- data: decodeReferralLink(encoded(address, data)).data
482
- }));
483
- }
484
- /** The vault's token balance. `0` when the vault does not exist yet. */
485
- async function fetchVaultBalance(client, mint, tokenProgram) {
486
- const vault = await vaultAddress(mint, tokenProgram);
417
+ const listDrawsByPool = async (client, pool) => await listDraws(client, { pool });
418
+ const listDrawsByPurchaser = async (client, purchaser) => await listDraws(client, { purchaser });
419
+ async function fetchVaultBalance(client, mint) {
420
+ const vault = await vaultAddress(mint);
487
421
  const { value } = await client.rpc.getAccountInfo(vault, {
488
422
  encoding: "base64",
489
423
  commitment: "confirmed"
490
424
  }).send();
491
- if (!value) return 0n;
492
- return tokenAccountAmount(new Uint8Array(base64$1.encode(value.data[0])));
425
+ return value ? tokenAccountAmount(new Uint8Array(base64.encode(value.data[0]))) : 0n;
493
426
  }
494
- /**
495
- * A pool with its live inventory. This is the pair every price display needs: the tiers are
496
- * immutable, but what they actually pay depends on what is free in the vault right now.
497
- *
498
- * # Why one request reads both accounts
499
- *
500
- * The oracle callback lowers the vault and `pool.reserved` in one transaction, a few seconds after
501
- * a buy. Two separate reads can land on either side of it: the old `reserved` with the new, smaller
502
- * vault. That pair looks insolvent, and `quote` rightly refuses it. One `getMultipleAccounts` call
503
- * returns both accounts from the same slot, so the pair is always one the chain actually held.
504
- */
505
427
  async function fetchPoolInventory(client, mint) {
506
- const address = await poolAddress(mint);
507
- const readPair = async (vault) => {
508
- const [poolAccount, vaultAccount] = await fetchEncodedAccounts(client.rpc, [address, vault], { commitment: "confirmed" });
509
- const pool = decodeCurrentPool(poolAccount);
510
- if (!pool) return null;
511
- return {
512
- pool,
513
- inventory: vaultAccount.exists ? tokenAccountAmount(vaultAccount.data) : 0n
514
- };
515
- };
516
- const expectedVault = await vaultAddress(mint);
517
- let pair = await readPair(expectedVault);
518
- if (pair && pair.pool.vault !== expectedVault) pair = await readPair(pair.pool.vault);
519
- if (!pair) return null;
520
- const { pool, inventory } = pair;
521
- const reserved = pool.reserved;
428
+ const poolPda = await poolAddress(mint);
429
+ const vaultPda = await vaultAddress(mint);
430
+ const { value } = await client.rpc.getMultipleAccounts([poolPda, vaultPda], {
431
+ encoding: "base64",
432
+ commitment: "confirmed"
433
+ }).send();
434
+ const poolAccount = value[0];
435
+ if (!poolAccount) return null;
436
+ const pool = decodeCurrentPool(encoded(poolPda, new Uint8Array(base64.encode(poolAccount.data[0]))));
437
+ if (!pool) return null;
438
+ if (pool.vault !== vaultPda) throw new Error(`pool ${poolPda} has an unexpected vault; it is not a supported Gabox pool`);
439
+ const vaultAccount = value[1];
440
+ const inventory = vaultAccount ? tokenAccountAmount(new Uint8Array(base64.encode(vaultAccount.data[0]))) : 0n;
522
441
  return {
442
+ poolAddress: poolPda,
523
443
  pool,
524
- poolAddress: address,
525
- vault: pool.vault,
526
444
  inventory,
527
- reserved,
528
- free: inventory >= reserved ? inventory - reserved : 0n
445
+ reserved: pool.reserved,
446
+ free: inventory > pool.reserved ? inventory - pool.reserved : 0n
529
447
  };
530
448
  }
531
- /** The generated `Tier` uses the same field names as `math.ts`, so this is only a widening. */
532
- const tiersOf = (pool) => pool.tiers.map((t) => ({
533
- multiplierBps: t.multiplierBps,
534
- tickets: t.tickets
449
+ const tiersOf = (pool) => pool.tiers.map(({ multiplierBps, tickets }) => ({
450
+ multiplierBps,
451
+ tickets
535
452
  }));
536
- /**
537
- * The offer a pack of `base` tokens would freeze against this inventory. Pure — the same
538
- * computation `buy_pack` performs, and the reason a client can show real amounts before paying.
539
- */
540
- const offerFor = (inventory, base) => quote(base, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
541
- //#endregion
542
- //#region src/compute.ts
543
- /**
544
- * The two `ComputeBudget` instructions the transaction builders prepend.
545
- *
546
- * # Why every gabox transaction needs one
547
- *
548
- * The default budget is 200,000 compute units for the whole transaction. `buy_pack` alone does a
549
- * CPI into Pump (which does its own CPIs into two token programs and its fee program), a
550
- * `transfer_checked`, an account init, a system transfer, and a CPI into MagicBlock's VRF program
551
- * that allocates a request account. `createMachine` adds Pump's `create_v2` to that, which mints a
552
- * Token-2022 coin with metadata. Neither fits in the default, and nothing on chain can raise its
553
- * own budget.
554
- *
555
- * # The numbers are headroom, not measurements
556
- *
557
- * They have not been measured on chain. They sit comfortably above what these instruction mixes
558
- * plausibly cost and comfortably below the 1,400,000 ceiling. **Measure them on devnet before they
559
- * matter**, because the prioritisation fee is `price x requested limit`: a request that is three
560
- * times too large costs three times too much on every pack. Until then, treat them as a starting
561
- * point a caller may override.
562
- */
563
- const COMPUTE_BUDGET_PROGRAM_ADDRESS = "ComputeBudget111111111111111111111111111111";
564
- /** The runtime's per-transaction ceiling. A larger request is rejected outright. */
565
- const MAX_COMPUTE_UNIT_LIMIT = 14e5;
566
- /** What an instruction gets when no `SetComputeUnitLimit` is present. */
567
- const DEFAULT_COMPUTE_UNIT_LIMIT = 2e5;
568
- /**
569
- * UNVERIFIED: none of the three figures below has been measured on chain. They are headroom, chosen
570
- * above what these instruction mixes plausibly cost. Measure them on devnet before they matter: the
571
- * prioritisation fee is `price x requested limit`, so a request three times too large costs three
572
- * times too much on every pack.
573
- */
574
- /** Pump `create_v2` plus `initialize_pool` plus a seed buy, in one transaction. */
575
- const CREATE_MACHINE_COMPUTE_UNITS = 6e5;
576
- /** A venue buy, an escrow transfer, a draw init and a VRF request. */
577
- const BUY_PACK_COMPUTE_UNITS = 5e5;
578
- /** A vault transfer, or a vault transfer plus a venue sale. */
579
- const REDEEM_COMPUTE_UNITS = 3e5;
580
- /** `bind_referrer` riding ahead of a pack: one small account init. */
581
- const BIND_REFERRER_COMPUTE_UNITS = 5e4;
582
- /** `ComputeBudgetInstruction`'s discriminants. Positional and append-only upstream. */
583
- const SET_COMPUTE_UNIT_LIMIT = 2;
584
- const SET_COMPUTE_UNIT_PRICE = 3;
585
- /** `[u8 discriminant, u32 units]` — five bytes. */
586
- const LIMIT_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["units", getU32Encoder()]]);
587
- /** `[u8 discriminant, u64 microLamports]` — nine bytes. */
588
- const PRICE_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["microLamports", getU64Encoder()]]);
589
- function getSetComputeUnitLimitInstruction(units) {
590
- if (!Number.isInteger(units) || units < 0 || units > 14e5) throw new Error(`compute unit limit must be an integer in 0..=${MAX_COMPUTE_UNIT_LIMIT}, got ${units}`);
591
- return {
592
- programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
593
- accounts: [],
594
- data: LIMIT_ENCODER.encode({
595
- discriminant: SET_COMPUTE_UNIT_LIMIT,
596
- units
597
- })
598
- };
599
- }
600
- /**
601
- * `SetComputeUnitPrice(microLamports)` — the priority fee, per compute unit.
602
- *
603
- * No default, deliberately. The fee that lands a transaction is a property of the network at the
604
- * moment you send it. A hardcoded price is either money burnt on an idle chain or a transaction
605
- * that quietly stops landing under load. Sample `getRecentPrioritizationFees`, or take it from
606
- * config.
607
- */
608
- function getSetComputeUnitPriceInstruction(microLamports) {
609
- const price = BigInt(microLamports);
610
- if (price < 0n) throw new Error(`compute unit price must not be negative, got ${price}`);
611
- return {
612
- programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
613
- accounts: [],
614
- data: PRICE_ENCODER.encode({
615
- discriminant: SET_COMPUTE_UNIT_PRICE,
616
- microLamports: price
617
- })
618
- };
619
- }
620
- /** The compute budget prefix a builder prepends: a limit, and a price only when one is asked for. */
621
- function computeBudgetInstructions(units, microLamports) {
622
- const instructions = [getSetComputeUnitLimitInstruction(units)];
623
- if (microLamports !== void 0) instructions.push(getSetComputeUnitPriceInstruction(microLamports));
624
- return instructions;
625
- }
453
+ const quotePool = (inventory) => quote(inventory.pool.packTokens, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
626
454
  //#endregion
627
455
  //#region src/events.ts
628
- /**
629
- * Events, and waiting for a draw to resolve.
630
- *
631
- * # Where the events are
632
- *
633
- * Anchor's `emit!` writes the event through `sol_log_data`, which the runtime renders as a log line
634
- * `Program data: <base64>`. So decoding an event is: find those lines, base64-decode each one,
635
- * match the first eight bytes against a discriminator, and hand the rest to the generated decoder.
636
- *
637
- * The design leans on this. There is no on-chain market registry, so `PoolCreated` is how a new
638
- * machine is discovered, and `DrawResolved` stays readable after the `Draw` account is closed and
639
- * its rent returned. The event is the audit trail; the account is only the pending state.
640
- *
641
- * # Waiting
642
- *
643
- * `watchDraw` subscribes to the draw account. A normal callback, retry callback, or expiry
644
- * delivers the prize and closes that account; the watcher then reconstructs the result from the
645
- * transaction's `PackBought` and `DrawResolved` events. It also accepts a legacy open `Ready`
646
- * account from a callback requested before automatic delivery.
647
- */
648
- const base64 = getBase64Encoder();
649
- const startsWith = (data, discriminator) => {
650
- if (data.length < discriminator.length) return false;
651
- for (let i = 0; i < discriminator.length; i++) if (data[i] !== discriminator[i]) return false;
652
- return true;
653
- };
654
- /** The prefix Anchor's `emit!` produces. Anything else in the log is not an event. */
655
- const PROGRAM_DATA = "Program data: ";
656
- /** Decode one `Program data:` payload, or `null` when it is not one of ours. */
456
+ /** Decode Gabox's Anchor events. Draw accounts are closed on delivery; use `DrawResolved` as final state. */
457
+ const b64 = getBase64Encoder();
458
+ const starts = (a, b) => a.length >= b.length && b.every((v, i) => a[i] === v);
657
459
  function decodeEvent(data) {
658
- if (startsWith(data, POOL_CREATED_EVENT_DISCRIMINATOR)) return {
460
+ if (starts(data, POOL_CREATED_EVENT_DISCRIMINATOR)) return {
659
461
  name: "PoolCreated",
660
462
  data: getPoolCreatedEventDecoder().decode(data)
661
463
  };
662
- if (startsWith(data, PRIZES_FUNDED_EVENT_DISCRIMINATOR)) return {
464
+ if (starts(data, PRIZES_FUNDED_EVENT_DISCRIMINATOR)) return {
663
465
  name: "PrizesFunded",
664
466
  data: getPrizesFundedEventDecoder().decode(data)
665
467
  };
666
- if (startsWith(data, PACK_BOUGHT_EVENT_DISCRIMINATOR)) return {
468
+ if (starts(data, PACK_BOUGHT_EVENT_DISCRIMINATOR)) return {
667
469
  name: "PackBought",
668
470
  data: getPackBoughtEventDecoder().decode(data)
669
471
  };
670
- if (startsWith(data, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR)) return {
472
+ if (starts(data, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR)) return {
671
473
  name: "RandomnessRetried",
672
474
  data: getRandomnessRetriedEventDecoder().decode(data)
673
475
  };
674
- if (startsWith(data, DRAW_RESOLVED_EVENT_DISCRIMINATOR)) return {
476
+ if (starts(data, DRAW_RESOLVED_EVENT_DISCRIMINATOR)) return {
675
477
  name: "DrawResolved",
676
478
  data: getDrawResolvedEventDecoder().decode(data)
677
479
  };
678
- if (startsWith(data, PRIZE_REDEEMED_EVENT_DISCRIMINATOR)) return {
679
- name: "PrizeRedeemed",
680
- data: getPrizeRedeemedEventDecoder().decode(data)
681
- };
682
- if (startsWith(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
480
+ if (starts(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
683
481
  name: "TokensSold",
684
482
  data: getTokensSoldEventDecoder().decode(data)
685
483
  };
686
484
  return null;
687
485
  }
486
+ const BASE58 = "[1-9A-HJ-NP-Za-km-z]+";
487
+ const INVOKE = new RegExp(`^Program (${BASE58}) invoke \\[(\\d+)\\]$`);
488
+ const SUCCESS = new RegExp(`^Program (${BASE58}) success$`);
489
+ const FAILED = new RegExp(`^Program (${BASE58}) failed: .*$`);
688
490
  /**
689
- * Every gabox event in a transaction's logs, in order.
690
- *
691
- * A log line that is not `Program data:`, or whose payload matches no discriminator, is skipped
692
- * rather than reported. Another program in the same transaction emits its own events, and they are
693
- * not an error here.
491
+ * Decode only Gabox events committed by successful runtime frames. Program logs are emitted before
492
+ * transaction commit and are forgeable by arbitrary programs, so `Program data` is authenticated
493
+ * by the canonical invoke/success stack and buffered until every enclosing frame succeeds.
694
494
  */
695
495
  function decodeEvents(logs) {
696
- const events = [];
496
+ const committed = [];
497
+ const stack = [];
498
+ const discardOpenFrames = () => {
499
+ stack.length = 0;
500
+ };
501
+ let malformed = false;
502
+ let transactionFailed = false;
697
503
  for (const line of logs) {
698
- if (!line.startsWith(PROGRAM_DATA)) continue;
699
- const payload = line.slice(14).trim();
700
- let bytes;
701
- try {
702
- bytes = new Uint8Array(base64.encode(payload));
703
- } catch {
504
+ const invoke = INVOKE.exec(line);
505
+ if (invoke) {
506
+ if (Number(invoke[2]) !== stack.length + 1) {
507
+ malformed = true;
508
+ discardOpenFrames();
509
+ continue;
510
+ }
511
+ stack.push({
512
+ programId: invoke[1],
513
+ pending: []
514
+ });
704
515
  continue;
705
516
  }
706
- const event = decodeEvent(bytes);
707
- if (event) events.push(event);
517
+ const success = SUCCESS.exec(line);
518
+ const failed = FAILED.exec(line);
519
+ if (success || failed) {
520
+ const programId = (success ?? failed)[1];
521
+ const frame = stack.at(-1);
522
+ if (!frame || frame.programId !== programId) {
523
+ malformed = true;
524
+ discardOpenFrames();
525
+ continue;
526
+ }
527
+ stack.pop();
528
+ if (success) {
529
+ const parent = stack.at(-1);
530
+ if (parent) parent.pending.push(...frame.pending);
531
+ else committed.push(...frame.pending);
532
+ }
533
+ if (failed && stack.length === 0) transactionFailed = true;
534
+ continue;
535
+ }
536
+ const frame = stack.at(-1);
537
+ if (!line.startsWith("Program data: ") || frame?.programId !== GABOX_PROGRAM_ID) continue;
538
+ try {
539
+ const event = decodeEvent(new Uint8Array(b64.encode(line.slice(14).trim())));
540
+ if (event) frame.pending.push(event);
541
+ } catch {}
708
542
  }
709
- return events;
543
+ return malformed || transactionFailed || stack.length !== 0 ? [] : committed;
710
544
  }
711
- /** Fetch a transaction and decode the gabox events it emitted. */
712
545
  async function fetchEvents(client, signature) {
713
546
  return await readEvents(client.rpc, signature);
714
547
  }
715
- /** The RPC half of `fetchEvents`, for the internal readers that already hold one. */
716
548
  async function readEvents(rpc, signature) {
717
- return decodeEvents((await rpc.getTransaction(signature, {
549
+ const tx = await rpc.getTransaction(signature, {
718
550
  commitment: "confirmed",
719
551
  encoding: "json",
720
552
  maxSupportedTransactionVersion: 0
721
- }).send())?.meta?.logMessages ?? []);
722
- }
723
- /**
724
- * Watch one draw and resolve when its callback delivers the prize.
725
- *
726
- * The first notification is the account as it is now, so a draw that resolved before the call
727
- * resolves the promise immediately. Automatic delivery closes the account, so the watcher rebuilds
728
- * the resolved snapshot from the purchase and resolution events when it sees that closure.
729
- *
730
- * There is no timeout here on purpose. The program's own deadline is 216,000 slots, which is about
731
- * a day, and a UI should decide its own patience rather than inherit one.
732
- */
733
- async function watchDraw(client, address, options = {}) {
734
- const { rpc, rpcSubscriptions } = client;
735
- const controller = new AbortController();
736
- const abort = () => controller.abort(options.signal?.reason);
737
- options.signal?.addEventListener("abort", abort, { once: true });
738
- try {
739
- const notifications = await rpcSubscriptions.accountNotifications(address, {
740
- encoding: "base64",
741
- commitment: "confirmed"
742
- }).subscribe({ abortSignal: controller.signal });
743
- const current = await readDraw(rpc, address);
744
- if (current) {
745
- options.onChange?.(current);
746
- if (current.status === 1) return current;
747
- } else {
748
- const delivered = await readDeliveredDraw(rpc, address);
749
- if (delivered) return delivered;
750
- }
751
- for await (const notification of notifications) {
752
- const account = notification.value;
753
- if (!account || account.data[0] === "") {
754
- for (let attempt = 0; attempt < 10; attempt++) {
755
- const delivered = await readDeliveredDraw(rpc, address);
756
- if (delivered) return delivered;
757
- await new Promise((resolve) => setTimeout(resolve, 500));
758
- }
759
- throw new Error(`draw ${address} closed before its resolution event became readable`);
553
+ }).send();
554
+ const meta = tx?.meta;
555
+ if (!tx || !meta || meta.err) return [];
556
+ return decodeEvents(meta.logMessages ?? []);
557
+ }
558
+ /** Poll final resolution events for one closed draw address. There is no separate Ready/claim state to poll instead. */
559
+ async function findResolvedDraw(client, address) {
560
+ let before;
561
+ for (let page = 0; page < 10; page++) {
562
+ const rows = await client.rpc.getSignaturesForAddress(address, {
563
+ commitment: "confirmed",
564
+ limit: 100,
565
+ ...before ? { before } : {}
566
+ }).send();
567
+ for (const row of rows) {
568
+ if (row.err) continue;
569
+ for (const event of await readEvents(client.rpc, row.signature)) {
570
+ if (event.name !== "DrawResolved") continue;
571
+ if (await drawAddress(event.data.pool, event.data.seq) === address) return {
572
+ ...event.data,
573
+ address
574
+ };
760
575
  }
761
- const draw = decodeDrawBytes(address, new Uint8Array(base64.encode(account.data[0])));
762
- options.onChange?.(draw);
763
- if (draw.status === 1) return draw;
764
576
  }
765
- throw new Error(`the subscription for draw ${address} ended before it resolved`);
766
- } finally {
767
- options.signal?.removeEventListener("abort", abort);
768
- controller.abort();
577
+ if (rows.length < 100) return null;
578
+ before = rows.at(-1)?.signature;
579
+ if (!before) return null;
769
580
  }
770
- }
771
- function decodeDrawBytes(address, data) {
772
- return decodeDraw({
773
- address,
774
- data,
775
- executable: false,
776
- lamports: 0n,
777
- programAddress: GABOX_PROGRAM_ID,
778
- space: BigInt(data.length)
779
- }).data;
780
- }
781
- async function readDraw(rpc, address) {
782
- const { value } = await rpc.getAccountInfo(address, {
783
- encoding: "base64",
784
- commitment: "confirmed"
785
- }).send();
786
- if (!value) return null;
787
- return decodeDrawBytes(address, new Uint8Array(base64.encode(value.data[0])));
788
- }
789
- /** Recover the final snapshot after automatic delivery closed the draw account. */
790
- async function readDeliveredDraw(rpc, address) {
791
- const signatures = await rpc.getSignaturesForAddress(address, {
792
- commitment: "confirmed",
793
- limit: 10
794
- }).send();
795
- const rows = await Promise.all(signatures.filter((row) => !row.err).map(async (row) => ({
796
- slot: row.slot,
797
- events: await readEvents(rpc, row.signature)
798
- })));
799
- const bought = rows.flatMap((row) => row.events.flatMap((event) => event.name === "PackBought" ? [{
800
- slot: row.slot,
801
- data: event.data
802
- }] : []))[0];
803
- const resolved = rows.flatMap((row) => row.events.flatMap((event) => event.name === "DrawResolved" ? [{
804
- slot: row.slot,
805
- data: event.data
806
- }] : []))[0];
807
- if (!bought || !resolved) return null;
808
- if (bought.data.pool !== resolved.data.pool || bought.data.seq !== resolved.data.seq || bought.data.purchaser !== resolved.data.purchaser) return null;
809
- const [derived, bump] = await findDrawPda({
810
- pool: bought.data.pool,
811
- seq: bought.data.seq
812
- });
813
- if (derived !== address) return null;
814
- const ticketed = bought.data.prizes.filter((prize) => prize.tickets > 0);
815
- if (ticketed.length === 0) return null;
816
- const lastRetry = rows.flatMap((row) => row.events.flatMap((event) => event.name === "RandomnessRetried" && event.data.pool === bought.data.pool && event.data.seq === bought.data.seq ? [{
817
- slot: row.slot,
818
- attempt: event.data.attempt
819
- }] : [])).reduce((latest, row) => !latest || row.slot > latest.slot ? row : latest, null);
820
- return {
821
- discriminator: DRAW_DISCRIMINATOR,
822
- pool: bought.data.pool,
823
- purchaser: bought.data.purchaser,
824
- seq: bought.data.seq,
825
- bump,
826
- status: 1,
827
- requestSlot: bought.slot,
828
- lastAttemptSlot: lastRetry?.slot ?? bought.slot,
829
- attempts: lastRetry?.attempt ?? 1,
830
- maximum: ticketed.reduce((a, prize) => a > prize.amount ? a : prize.amount, 0n),
831
- minimum: ticketed.reduce((a, prize) => a < prize.amount ? a : prize.amount, ticketed[0].amount),
832
- prizes: bought.data.prizes,
833
- amount: resolved.data.amount,
834
- randomness: resolved.data.randomness,
835
- timedOut: resolved.data.timedOut
836
- };
581
+ return null;
837
582
  }
838
583
  //#endregion
839
584
  //#region src/lookupTables.ts
840
585
  /**
841
- * Shared devnet address lookup table, verified on 2026-09-10T13:05:37.919Z.
586
+ * Shared devnet address lookup table, verified on 2026-09-18T07:26:38.218Z.
842
587
  * Generated by scripts/deploy-lookup-table.ts. Existing indices are immutable;
843
588
  * keep this table active while clients use it. Authority is the devnet deploy wallet.
844
589
  */
@@ -917,7 +662,47 @@ const DEVNET_LOOKUP_TABLE_ADDRESSES = [
917
662
  address("Freijj9xKLefjrb5fHgT6KMbYG1XBP2mA83tqeXYUMYM"),
918
663
  address("4uzPz9TPskXiiEZ6X78rqud8LvfdxkJBr5EKHgbx4azP"),
919
664
  address("Hxzab4UjjVH2KjsdAqzdxGdYUpNN5FKhpu7iikB869uH"),
920
- address("Frkwunr9dQM9d4TthfQ2unxC994XpkLMpkRP2e7yfirk")
665
+ address("Frkwunr9dQM9d4TthfQ2unxC994XpkLMpkRP2e7yfirk"),
666
+ address("E6ShohW57z5CJPBeEcFAEbvPqUyt6QxHcxnkh4hMaNrg"),
667
+ address("6z6GDdfb2AjR9ZhJmAUQ5cipJCVxQvLJhB2H8mCwTFBP"),
668
+ address("D9LwzTvJ5XoGxorcdaZvBgq7Qruqewu2P9WVsgSrKURd"),
669
+ address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"),
670
+ address("GCow1cTVRa43v8EX1eVwVobiKXoyZjfQpwAvgpZ8B2KP"),
671
+ address("65LkFkYo9gMD6AwXTbSsxR3d28pCVbJp5AtE9NK3634n"),
672
+ address("8wDZLhae9WbNvm3eCgHVDrXUhJbd3gx6DnTnhsw45XEj"),
673
+ address("CPgoAkfWjiUZfNLRp94hD7BjmDSFy96mS8xcQKoPFB4H"),
674
+ address("GXVKyyXsoUihF1uzGCx7YXVqQ7kimUDAo3xyqULvgDyB"),
675
+ address("5tpFHeni6NYD6pQpkuReVPGtCJLyaXDq97FTJJzyZMjt"),
676
+ address("DNscAYMk2LW55Aq7FTj24mRbwVPZ6EgvCUoabhhtdVCW"),
677
+ address("7Q2wpniGesAyAkjACpg58BAcDjBKcdcX9a6RAgszVh4M"),
678
+ address("7gx9ZEwMSd1ifHBEgVaUsCkQmHTFJTWVs3C1bQABQ22T"),
679
+ address("GsVBKjffkB769p9tHTZWoAX3r9T6dXoDTJr3f7XutJH7"),
680
+ address("9NFrxdnmedHKHs1tnhYm9G5XTJh7Lt7xN6uxgZzwQNM7"),
681
+ address("BhMknQ4j9RZUbJk6GS4QJh8MSKxcqZHS2x9MZh2AH9hA"),
682
+ address("9NW32ymMo8Qx6DTbgkxtnD8Dh9hssQYpcyY2Brdpg2hs"),
683
+ address("5a8Gfgwx4hrCtYKgvjtX57FsirXPRN7Jzm9aXmn6hQs8"),
684
+ address("5KUNmCZatysY7fxLtTgo2bpkqevPRZZG8fkrh3e1P89F"),
685
+ address("HPfEytxa5JGqmGiVwrSepAcNTkWboxvtQKbyWN9DNCoQ"),
686
+ address("Gr5kHfDBd7GAdjK6Ct3EDC566XFPjCr3mLCkKVxJYrMD"),
687
+ address("HzCwuA6T48enyWvCJWrLhNvMpJJR43kddjMGFf6LyA9L"),
688
+ address("Do4esSd37h4uHz35piua8rRtcXDNt27re8NDqqZfjsGJ"),
689
+ address("2GC6K75FS6MpRrMZYXifinC2sVXP3htsJpayerx7ACci"),
690
+ address("FvEpzodkyvMzRHQo4q7fhvfa287hnjfr1oiumRtQXrse"),
691
+ address("8SwxZnhYHeC9S93ZFzLTd9dC2c44hfCdGyoXYzA7U6Db"),
692
+ address("6B1oS2LSDYHyXpRF9S8C6r5LffitosDrhie2WWsUtfRV"),
693
+ address("F2pzDCn3vqXcNWF6osLxSvVtf3CyTkHd4tRxhGjyRZQ9"),
694
+ address("DKESpHxobrT9Ra4snSXCD4cdhQMqpzmGhZN1HZ3sUYMY"),
695
+ address("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
696
+ address("SysvarRent111111111111111111111111111111111"),
697
+ address("DRay6fNdQ5J82H7xV6uq2aV3mNrUZ1J4PgSKsWgptcm6"),
698
+ address("5xqNaZXX5eUi4p5HU4oz9i5QnwRNT2y6oN7yyn4qENeq"),
699
+ address("4uAB7seenFJKPUXqYewAdfra2u6baBgjiXU8x1SC7Ycz"),
700
+ address("7ZR4zD7PYfY2XxoG1Gxcy2EgEeGYrpxrwzPuwdUBssEt"),
701
+ address("DdEeCPXbCAzHE2PZSoR3RZng4WA4bSztrezQznrJ4ooB"),
702
+ address("DRaycpLY18LhpbydsBWbVJtxpNv9oXPgjRSfpF2bWpYb"),
703
+ address("CXniRufdq5xL8t8jZAPxsPZDpuudwuJSPWnbcD5Y5Nxq"),
704
+ address("G7YfJJp1TX1VtzN4V2yhPNSU23AKPSy1U2miRdwAByK5"),
705
+ address("5WcPTEQ59UqpQzjZUPbU8QRGCbj7NeQNLDa7DbsLkLKT")
921
706
  ];
922
707
  const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET_LOOKUP_TABLE_ADDRESSES] };
923
708
  /**
@@ -950,10 +735,10 @@ async function getOffer(client, mint, options = {}) {
950
735
  }
951
736
  /**
952
737
  * The same computation with the reads already done. Useful when a caller holds a `ResolvedVenue`
953
- * and wants to re-price without touching the network. `quoteLamports` is
738
+ * and wants to re-price without touching the network. `quoteAmount` is
954
739
  * `venue.quoteBuy(pool.packTokens)`.
955
740
  */
956
- function offerFromState(inventory, venue, quoteLamports) {
741
+ function offerFromState(inventory, venue, quoteAmount) {
957
742
  const { pool } = inventory;
958
743
  const tiers = tiersOf(pool);
959
744
  const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
@@ -962,12 +747,10 @@ function offerFromState(inventory, venue, quoteLamports) {
962
747
  mint: pool.mint,
963
748
  pool: inventory.poolAddress,
964
749
  packTokens: pool.packTokens,
965
- quoteLamports,
966
- feeBps: pool.feeBps,
967
- feeLamports: share(quoteLamports, BigInt(pool.feeBps)),
968
- protocolLamports: share(quoteLamports, PROTOCOL_FEE_BPS),
969
- seedLamports: pool.seedLamports,
970
- seedTokens: pool.seedTokens,
750
+ quoteAmount,
751
+ quoteMint: pool.quoteMint,
752
+ seedQuoteAmount: pool.seedQuoteAmount,
753
+ seedTokens: seedTokens(pool.packTokens, tiers),
971
754
  venue,
972
755
  prizes: offer.prizes,
973
756
  maximum: offer.maximum,
@@ -1054,8 +837,8 @@ function clusterNamedBy(url) {
1054
837
  */
1055
838
  function assertClusterUrl(cluster, url) {
1056
839
  const named = clusterNamedBy(url);
1057
- if (cluster === "devnet" && named !== "devnet") throw new Error(`refusing to use ${url} as a devnet endpoint: it does not name devnet.\nEvery address in this SDK — the Pump programs, the MagicBlock queue, the pools exists on every cluster, so a wrong URL is a live transaction, not an error. Pass { cluster: 'localnet' } for a local validator, or name the cluster the URL really is.`);
1058
- if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\nEvery address in this SDK exists on every cluster, so a wrong URL is a live transaction, not an error. Pass the cluster the URL really names.`);
840
+ if (cluster === "devnet" && named !== "devnet") throw new Error(`refusing to use ${url} as a devnet endpoint: it does not name devnet.\nA Gabox program id, a MagicBlock queue and a pool address all exist on every cluster, so a wrong URL is a live transaction, not an error. Pass { cluster: 'localnet' } for a local validator, or name the cluster the URL really is.`);
841
+ if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\nA Gabox address exists on every cluster, so a wrong URL is a live transaction, not an error. Pass the cluster the URL really names.`);
1059
842
  }
1060
843
  /** `https://x` becomes `wss://x`, `http://x` becomes `ws://x`. Anything else is returned as is. */
1061
844
  function websocketUrlFor(url) {
@@ -1087,307 +870,201 @@ function createClient(config) {
1087
870
  };
1088
871
  }
1089
872
  //#endregion
1090
- //#region src/tx/message.ts
873
+ //#region src/tx/wsol.ts
1091
874
  /**
1092
- * Assembling a transaction message.
875
+ * Wrapping and unwrapping SOL around a venue trade.
1093
876
  *
1094
- * Every builder in this directory ends here: compute budget first, then the program instructions,
1095
- * with a fee payer and a blockhash lifetime. The result is a message a wallet can sign and send —
1096
- * nothing in this SDK signs or sends anything itself.
1097
- */
1098
- /**
1099
- * Build the message. One RPC read, for the blockhash.
877
+ * Both venues settle in WSOL, never in native SOL. So every builder in this directory does the same
878
+ * three things around its Gabox instruction:
1100
879
  *
1101
- * The blockhash expires in about a minute, so build the message when the user is ready to sign
1102
- * rather than when the page loads.
880
+ * 1. create the wallet's WSOL associated token account, if it is missing;
881
+ * 2. move the lamports it is going to spend into that account and `syncNative` it, so the token
882
+ * balance matches the lamports;
883
+ * 3. close the account afterwards, which sends everything left back to the wallet as SOL.
884
+ *
885
+ * A sale needs no step 2: the proceeds arrive in the account, and the close is what turns them into
886
+ * SOL.
887
+ *
888
+ * # Closing unwraps everything
889
+ *
890
+ * If the wallet already held WSOL in that account, the close turns that into SOL too. Nothing is
891
+ * lost and the wallet still owns every lamport, but the balance moves out of the token account. A
892
+ * wallet that keeps a WSOL position on purpose should build its own instructions instead of using
893
+ * these builders.
1103
894
  */
1104
- async function buildMessage(client, feePayer, instructions, options) {
1105
- const { value: latestBlockhash } = await client.rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
1106
- const budget = computeBudgetInstructions(options.computeUnitLimit, options.computeUnitPrice);
1107
- const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(feePayer, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions([...budget, ...instructions], m), (m) => compressTransactionMessageUsingAddressLookupTables(m, options.addressLookupTables ?? client.addressLookupTables));
1108
- const size = getTransactionEncoder().encode(compileTransaction(message)).length;
1109
- if (size > 1232) throw new Error(`Transaction is ${size} bytes; Solana allows 1232. Shorten metadata or supply additional address lookup tables.`);
1110
- return message;
1111
- }
1112
- /** Append `remainingAccounts` to a generated instruction, which is how a venue's list is passed. */
1113
- function withRemainingAccounts(instruction, remaining) {
1114
- return {
1115
- ...instruction,
1116
- accounts: [...instruction.accounts ?? [], ...remaining]
1117
- };
1118
- }
1119
- //#endregion
1120
- //#region src/referral.ts
1121
- /** Resolve the purchaser's permanent referral binding for a pack purchase. */
1122
- async function resolveReferral(client, purchaser, pool) {
1123
- const link = await referralLinkAddress(purchaser);
1124
- const account = await fetchMaybeReferralLink(client.rpc, link, { commitment: "confirmed" });
1125
- if (!account.exists) return null;
1126
- const referrer = account.data.referrer;
895
+ /** The wallet's WSOL account, and the instructions that put `lamports` of spendable WSOL in it. */
896
+ async function fundWsol(owner, lamports) {
897
+ if (lamports < 0n) throw new Error("lamports must not be negative");
898
+ const account = await wsolAccountFor(owner.address);
899
+ const instructions = [getCreateAssociatedTokenIdempotentInstruction({
900
+ payer: owner,
901
+ ata: account,
902
+ owner: owner.address,
903
+ mint: WSOL_MINT,
904
+ tokenProgram: TOKEN_PROGRAM_ADDRESS
905
+ })];
906
+ if (lamports > 0n) instructions.push(getTransferSolInstruction({
907
+ source: owner,
908
+ destination: account,
909
+ amount: lamports
910
+ }), getSyncNativeInstruction({ account }));
1127
911
  return {
1128
- link,
1129
- referrer,
1130
- referral: await referralAddress(pool, referrer)
912
+ account,
913
+ instructions
1131
914
  };
1132
915
  }
1133
- /** Bind a wallet to a referrer. The purchaser signs and the binding is permanent. */
1134
- async function bindReferrer(client, referee, referrer, options = {}) {
1135
- return await buildMessage(client, referee, [await getBindReferrerInstructionAsync({
1136
- referee,
1137
- referrer
1138
- })], {
1139
- addressLookupTables: options.addressLookupTables,
1140
- computeUnitLimit: options.computeUnitLimit ?? 8e4,
1141
- ...options.computeUnitPrice === void 0 ? {} : { computeUnitPrice: options.computeUnitPrice }
1142
- });
1143
- }
1144
- /** Claim all accrued referral rewards for one pool. */
1145
- async function claimReferral(client, referrer, pool, options = {}) {
1146
- return await buildMessage(client, referrer, [await getClaimReferralInstructionAsync({
1147
- referrer,
1148
- pool
1149
- })], {
1150
- addressLookupTables: options.addressLookupTables,
1151
- computeUnitLimit: options.computeUnitLimit ?? 8e4,
1152
- ...options.computeUnitPrice === void 0 ? {} : { computeUnitPrice: options.computeUnitPrice }
916
+ /** Close the WSOL account, sending every lamport in it back to the owner as SOL. */
917
+ function unwrapWsol(owner, account) {
918
+ return getCloseAccountInstruction({
919
+ account,
920
+ destination: owner.address,
921
+ owner
1153
922
  });
1154
923
  }
1155
924
  //#endregion
1156
925
  //#region src/tx/buyPack.ts
1157
- /**
1158
- * Buying a pack.
1159
- *
1160
- * One instruction does the whole thing: buy exactly `pool.packTokens` at the venue, move the
1161
- * tokens into gabox custody, freeze a prize table into a new `Draw`, pay the creator's fee and
1162
- * the protocol's 1% of the venue cost, and ask MagicBlock for randomness. Either all of it happens
1163
- * or none of it does.
1164
- *
1165
- * # The three numbers the buyer signs
1166
- *
1167
- * `maxQuoteIn` is the slippage cap at the venue, in lamports (WSOL on PumpSwap). The token count
1168
- * is fixed by the pool, so the cost is the only thing that moves. Set it from `offer.quoteLamports`
1169
- * plus a margin, for example 2%.
1170
- *
1171
- * `minMaximum` is a floor on the top prize, in tokens. The table is fixed, so only an inventory
1172
- * cap can lower it. Set it from `offer.maximum`.
1173
- *
1174
- * `maxTotalDebit` caps the venue debit plus the creator fee plus the protocol fee plus the VRF
1175
- * request, measured in SOL and WSOL together. Both fees are a share of the venue debit, so budget
1176
- * them from `maxQuoteIn`. It does **not** cover the rent Anchor pays for the `Draw` and any ATA it
1177
- * creates, nor the transaction fee: the runtime charges those before the handler runs. Budget for
1178
- * them separately.
1179
- *
1180
- * # PumpSwap needs WSOL first
1181
- *
1182
- * On the curve, Pump spends native SOL. On PumpSwap the quote is a classic WSOL token account, so
1183
- * the buyer's WSOL ATA must exist and hold enough before the pack instruction runs. This builder
1184
- * adds the create/fund/sync instructions when the route is PumpSwap. They are the buyer's own
1185
- * instructions, signed by the buyer, and gabox never touches that account.
1186
- */
1187
926
  async function buyPack(client, input) {
1188
- const { mint, purchaser } = input;
1189
927
  if (input.maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
1190
- const inventory = await fetchPoolInventory(client, mint);
1191
- if (!inventory) throw new Error(`no gabox pool for mint ${mint}`);
928
+ if (input.maxNativeDebit < 0n) throw new Error("maxNativeDebit must not be negative");
929
+ const inventory = await fetchPoolInventory(client, input.mint);
930
+ if (!inventory) throw new Error(`no Gabox pool for mint ${input.mint}`);
1192
931
  const { pool, poolAddress } = inventory;
932
+ const purchaser = input.purchaser.address;
1193
933
  const venue = await resolveVenue(client, {
1194
- mint,
1195
- user: purchaser.address,
934
+ mint: input.mint,
935
+ user: purchaser,
1196
936
  ...input.venue ? { venue: input.venue } : {}
1197
937
  });
1198
938
  const draw = await drawAddress(poolAddress, input.seq ?? pool.nextSeq);
1199
- let referral = await resolveReferral(client, purchaser.address, poolAddress);
1200
- let bind = null;
1201
- if (referral === null && input.referrer && canBind(purchaser.address, input.referrer)) {
1202
- const referrer = input.referrer;
1203
- bind = await getBindReferrerInstructionAsync({
1204
- referee: purchaser,
1205
- referrer
1206
- });
1207
- referral = {
1208
- link: await referralLinkAddress(purchaser.address),
1209
- referrer,
1210
- referral: await referralAddress(poolAddress, referrer)
1211
- };
1212
- }
1213
- const buy = getBuyPackInstruction({
1214
- purchaser,
939
+ const buy = await getBuyPackInstructionAsync({
940
+ purchaser: input.purchaser,
1215
941
  pool: poolAddress,
1216
- creator: pool.creator,
1217
942
  draw,
1218
- mint,
943
+ mint: input.mint,
944
+ quoteMint: pool.quoteMint,
1219
945
  vault: pool.vault,
1220
- userTokens: venueUserTokens(venue.buyAccounts, venue.kind),
1221
946
  venue: venue.program,
1222
- identity: await vrfIdentityAddress(),
1223
- tokenProgram: pool.tokenProgram,
1224
947
  maxQuoteIn: input.maxQuoteIn,
1225
948
  minMaximum: input.minMaximum,
1226
- maxTotalDebit: input.maxTotalDebit,
1227
- referralLink: referral?.link,
1228
- referral: referral?.referral
949
+ maxNativeDebit: input.maxNativeDebit
1229
950
  });
1230
- const remaining = input.cashback ? [...venue.buyAccounts, swapCashbackAccount(purchaser.address)] : venue.buyAccounts;
1231
- return await buildMessage(client, purchaser, [
1232
- ...bind === null ? [] : [bind],
1233
- ...venue.kind === "pumpswap" ? wsolPreparation$2(purchaser, input.wrapLamports ?? input.maxQuoteIn) : [],
1234
- withRemainingAccounts(buy, remaining)
1235
- ], {
951
+ const wsol = await fundWsol(input.purchaser, input.maxQuoteIn);
952
+ const instructions = [
953
+ ...wsol.instructions,
954
+ withRemainingAccounts(buy, venue.buyAccounts),
955
+ unwrapWsol(input.purchaser, wsol.account)
956
+ ];
957
+ return await buildMessage(client, input.purchaser, instructions, {
1236
958
  addressLookupTables: input.addressLookupTables,
1237
- computeUnitLimit: input.computeUnitLimit ?? 5e5 + (bind === null ? 0 : 5e4),
959
+ computeUnitLimit: input.computeUnitLimit ?? 24e4,
1238
960
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1239
961
  });
1240
962
  }
1241
- /** The default `Pubkey`: all zero bytes. `bind_referrer` refuses a link pointing at it. */
1242
- const NO_REFERRER = "11111111111111111111111111111111";
1243
- /**
1244
- * The program refuses a self-referral and a link to the all-zero address. Both would take the
1245
- * pack down with them, so the bind is skipped instead and the pack goes through unreferred.
1246
- */
1247
- function canBind(purchaser, referrer) {
1248
- return referrer !== purchaser && referrer !== NO_REFERRER;
1249
- }
1250
- /**
1251
- * The purchaser's base-token ATA, taken out of the venue account list rather than re-derived.
1252
- *
1253
- * `market.rs` requires `user_tokens` to equal the venue list's own `associated_base_user` (Pump) or
1254
- * `user_base_token_account` (PumpSwap). Reading it back from the list makes the two agree by
1255
- * construction instead of by two derivations that happen to match.
1256
- */
1257
- function venueUserTokens(accounts, kind) {
1258
- const account = accounts[kind === "pump" ? 14 : 5];
1259
- if (!account) throw new Error(`the ${kind} account list is too short`);
1260
- return account.address;
1261
- }
1262
- /**
1263
- * Create the buyer's WSOL account if needed, fund it, and sync its balance.
1264
- *
1265
- * `SyncNative` is the step people forget. A plain SOL transfer into a WSOL account raises its
1266
- * lamports but not the `amount` field the token program reads, so the venue sees an empty account
1267
- * until this instruction copies one to the other.
1268
- */
1269
- function wsolPreparation$2(purchaser, lamports) {
1270
- const wsol = wsolAccountFor(purchaser.address);
1271
- return [
1272
- getCreateAssociatedTokenIdempotentInstruction({
1273
- payer: purchaser,
1274
- ata: wsol,
1275
- owner: purchaser.address,
1276
- mint: WSOL_MINT,
1277
- tokenProgram: TOKEN_PROGRAM_ADDRESS$1
1278
- }),
1279
- getTransferSolInstruction({
1280
- source: purchaser,
1281
- destination: wsol,
1282
- amount: lamports
1283
- }),
1284
- getSyncNativeInstruction({ account: wsol })
1285
- ];
1286
- }
1287
963
  //#endregion
1288
964
  //#region src/tx/createMachine.ts
1289
965
  /**
1290
- * Creating a machine: one transaction, two instructions, two signers.
1291
- *
1292
- * # Why it has to be one transaction
1293
- *
1294
- * `initialize_pool` reads the Instructions sysvar and refuses to run unless the same transaction
1295
- * also carries a Pump `create_v2` for the same mint, signed by the same creator. That is what makes
1296
- * "one pool per coin" true and stops anyone wrapping an existing coin in a machine. The create must
1297
- * come **first**: the mint account has to exist and deserialize before Anchor validates
1298
- * `initialize_pool`'s accounts.
1299
- *
1300
- * # The seed
1301
- *
1302
- * The creator owns none of the coin yet — it does not exist until this transaction runs. So
1303
- * `initialize_pool` buys the seed on the curve itself, into the creator's own ATA, and moves it
1304
- * straight into the vault. The Pump buy accounts ride along as `remainingAccounts`, in the same
1305
- * order `buy_pack` uses.
1306
- *
1307
- * The seed follows from the tier table. The program accepts any table that passes `math::validate`;
1308
- * it does not enforce one table. A creator may pass their own `tiers`; the default is
1309
- * `DEFAULT_TIERS`. The program buys exactly `largestTierAmount - PACK_TOKENS` tokens as the seed,
1310
- * so the first pack can pay the top tier in full. The creator only signs a maximum SOL cost for
1311
- * that buy. The seed buy moves the curve, so a bigger jackpot means a slightly higher starting
1312
- * pack price. A fresh Pump curve only holds so many tokens (`Global.initialRealTokenReserves`),
1313
- * so a table whose seed exceeds that cannot be created; `createMachine` and `seedCostEstimate`
1314
- * both check this before spending anything.
1315
- *
1316
- * # Two signers
1317
- *
1318
- * The mint keypair signs `create_v2` — Pump takes it as a signer rather than deriving it — and the
1319
- * creator signs both instructions and pays for everything.
1320
- */
1321
- /**
1322
966
  * Build the transaction message. Sign it with both `creator` and `mintKeypair`.
1323
967
  *
1324
- * Reads Pump's `Global` account, because two of the buy accounts the fee recipient and the
1325
- * buyback fee recipient — are chosen from lists held there. Nothing else needs the chain: the coin
1326
- * does not exist yet, so every other account is a derivation.
968
+ * Reads LaunchLab's global config and the Gabox platform config, because the seed price depends on
969
+ * their fee rates. Nothing else needs the chain: the coin does not exist yet, so every other
970
+ * account is a derivation.
1327
971
  */
1328
972
  async function createMachine(client, input) {
1329
- const { creator, mintKeypair, name, symbol, uri, feeBps, maxSeedLamports } = input;
1330
- if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 100) throw new Error(`feeBps must be a whole number from 0 to 100`);
973
+ const { creator, mintKeypair, name, symbol, uri, maxSeedQuoteIn, maxSeedNativeDebit } = input;
1331
974
  const tiers = cloneTiers(input.tiers ?? DEFAULT_TIERS);
1332
975
  validateTiers(tiers);
1333
976
  validatePack(PACK_TOKENS, tiers);
1334
- const seed = seedTokens(PACK_TOKENS, tiers);
1335
- if (seed > 0n && maxSeedLamports <= 0n) throw new Error("maxSeedLamports must be positive when the jackpot needs a seed");
1336
- await assertSeedFitsFreshCurve(client, seed);
977
+ const extraSeedTokens = input.extraSeedTokens ?? 0n;
978
+ if (extraSeedTokens < 0n) throw new Error("extraSeedTokens must not be negative");
979
+ const seed = seedTokens(PACK_TOKENS, tiers) + extraSeedTokens;
980
+ assertSeedFitsCurve(seed);
981
+ if (seed > 0n && maxSeedQuoteIn <= 0n) throw new Error("maxSeedQuoteIn must be positive when the jackpot needs a seed");
982
+ if (maxSeedQuoteIn < 0n) throw new Error("maxSeedQuoteIn must not be negative");
983
+ if (maxSeedNativeDebit < 0n) throw new Error("maxSeedNativeDebit must not be negative");
984
+ const ids = raydiumIds(client.cluster);
1337
985
  const mint = mintKeypair.address;
1338
- const create = getPumpCreateV2Instruction({
986
+ const quoteMint = WSOL_MINT;
987
+ const create = await getLaunchInstruction({
1339
988
  mint: mintKeypair,
1340
- user: creator,
989
+ creator,
1341
990
  name,
1342
991
  symbol,
1343
- uri,
1344
- creator: creator.address,
1345
- mayhemMode: false,
1346
- cashback: false
1347
- });
1348
- const venueAccounts = await pumpSeedBuyAccounts(client, {
992
+ uri
993
+ }, ids);
994
+ const poolState = await launchlabPoolAddress(ids.launchlab, mint, quoteMint);
995
+ const venueAccounts = launchlabBuyAccounts({
996
+ launchlab: ids.launchlab,
997
+ launchlabAuthority: ids.launchlabAuthority,
998
+ launchlabEventAuthority: ids.launchlabEventAuthority,
999
+ globalConfig: ids.solGlobalConfig,
1000
+ platformConfig: ids.gaboxPlatform,
1001
+ poolState,
1349
1002
  mint,
1003
+ quoteMint,
1004
+ baseVault: await launchlabVaultAddress(ids.launchlab, poolState, mint),
1005
+ quoteVault: await launchlabVaultAddress(ids.launchlab, poolState, quoteMint),
1350
1006
  user: creator.address,
1351
- ...input.feeRecipientIndex === void 0 ? {} : { feeRecipientIndex: input.feeRecipientIndex },
1352
- ...input.buybackRecipientIndex === void 0 ? {} : { buybackRecipientIndex: input.buybackRecipientIndex }
1007
+ userBaseToken: await ata(creator.address, mint),
1008
+ userQuoteToken: await ata(creator.address, quoteMint),
1009
+ platformFeeVault: await platformFeeVaultAddress(ids.launchlab, ids.gaboxPlatform, quoteMint),
1010
+ creatorFeeVault: await creatorFeeVaultAddress(ids.launchlab, creator.address, quoteMint)
1353
1011
  });
1354
- return await buildMessage(client, creator, [create, withRemainingAccounts(await getInitializePoolInstructionAsync({
1012
+ const initialize = await getInitializePoolInstructionAsync({
1355
1013
  creator,
1356
1014
  mint,
1357
- tokenProgram: PUMP_CREATE_TOKEN_PROGRAM_ADDRESS,
1358
- venue: PUMP_PROGRAM_ADDRESS,
1359
- feeBps,
1015
+ quoteMint,
1016
+ venue: ids.launchlab,
1360
1017
  tiers,
1361
- maxSeedLamports
1362
- }), venueAccounts)], {
1018
+ maxSeedQuoteIn,
1019
+ maxSeedNativeDebit,
1020
+ extraSeedTokens
1021
+ });
1022
+ const wsol = await fundWsol(creator, maxSeedQuoteIn);
1023
+ const instructions = [
1024
+ create,
1025
+ ...wsol.instructions,
1026
+ withRemainingAccounts(initialize, venueAccounts),
1027
+ unwrapWsol(creator, wsol.account)
1028
+ ];
1029
+ return await buildMessage(client, creator, instructions, {
1363
1030
  addressLookupTables: input.addressLookupTables,
1364
- computeUnitLimit: input.computeUnitLimit ?? 6e5,
1031
+ computeUnitLimit: input.computeUnitLimit ?? 385e3,
1365
1032
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1366
1033
  });
1367
1034
  }
1368
1035
  /**
1369
1036
  * What the seed for this table costs, fees included, and how many tokens it is.
1370
1037
  *
1371
- * The coin does not exist yet, so the price is Pump's default new curve. Nothing else trades on
1372
- * it before `initialize_pool` runs in the same transaction, so this is exact up to a change in
1373
- * Pump's fee settings between the read and the send. Add a small margin for `maxSeedLamports`.
1038
+ * The coin does not exist yet, so the price comes from the starting reserves LaunchLab derives from
1039
+ * the pinned launch shape. Nothing trades on the curve before `initialize_pool` runs in the same
1040
+ * transaction, so this is exact up to a change in Raydium's fee rates between the read and the
1041
+ * send.
1374
1042
  *
1375
- * Defaults to `DEFAULT_TIERS`. Throws if `tiers` fails `validateTiers`/`validatePack`, or if the
1376
- * seed is bigger than a fresh Pump curve can sell in one buy.
1043
+ * Defaults to `DEFAULT_TIERS` and no extra seed. Throws if `tiers` fails
1044
+ * `validateTiers`/`validatePack`, if `extraSeedTokens` is negative, or if the total seed is bigger
1045
+ * than the curve sells.
1377
1046
  */
1378
- async function seedCostEstimate(client, tiers = DEFAULT_TIERS) {
1047
+ async function seedCostEstimate(client, tiers = DEFAULT_TIERS, options = {}) {
1379
1048
  const copiedTiers = cloneTiers(tiers);
1380
1049
  validateTiers(copiedTiers);
1381
1050
  validatePack(PACK_TOKENS, copiedTiers);
1382
- const seed = seedTokens(PACK_TOKENS, copiedTiers);
1383
- await assertSeedFitsFreshCurve(client, seed);
1051
+ const extraSeedTokens = options.extraSeedTokens ?? 0n;
1052
+ if (extraSeedTokens < 0n) throw new Error("extraSeedTokens must not be negative");
1053
+ const mandatorySeed = seedTokens(PACK_TOKENS, copiedTiers);
1054
+ const seed = mandatorySeed + extraSeedTokens;
1055
+ assertSeedFitsCurve(seed);
1056
+ const ids = raydiumIds(client.cluster);
1057
+ const settings = await fetchCurveSettings(client, ids);
1384
1058
  return {
1385
1059
  tiers: copiedTiers,
1386
- seedTokens: seed,
1387
- lamports: seed === 0n ? 0n : await newCurveBuyCost(client, seed)
1060
+ seedTokens: mandatorySeed,
1061
+ extraSeedTokens,
1062
+ totalSeedTokens: seed,
1063
+ quoteAmount: seed === 0n ? 0n : curveBuyExactOut(newCurveReserves(ids, settings.migrateFee), settings.rates, seed),
1064
+ quoteMint: WSOL_MINT
1388
1065
  };
1389
1066
  }
1390
- /** A mutable encoded-table shape, owned by this call and safe to pass to Codama's builder. */
1067
+ /** A mutable table shape, owned by this call and safe to pass to Codama's builder. */
1391
1068
  function cloneTiers(tiers) {
1392
1069
  return tiers.map(({ multiplierBps, tickets }) => ({
1393
1070
  multiplierBps,
@@ -1395,67 +1072,25 @@ function cloneTiers(tiers) {
1395
1072
  }));
1396
1073
  }
1397
1074
  /**
1398
- * Pump's quote silently caps at the curve's real token reserves, so a seed above them would look
1399
- * cheap instead of failing. A fresh curve holds `Global.initialRealTokenReserves`
1400
- * (793,100,000 tokens today), so a jackpot above about 794x of a 1,000,000-token pack cannot be
1401
- * seeded. This throws before any buy is attempted.
1075
+ * The curve sells `LAUNCH_TOTAL_BASE_SELL` coins in total, so a seed above that cannot be bought at
1076
+ * any price. A buy for more than the curve has left is capped rather than refused, so it would look
1077
+ * cheap instead of failing. This throws before any transaction is built.
1402
1078
  */
1403
- async function assertSeedFitsFreshCurve(client, seed) {
1404
- if (seed === 0n) return;
1405
- const { value } = await client.rpc.getAccountInfo(PUMP_GLOBAL, {
1406
- encoding: "base64",
1407
- commitment: "confirmed"
1408
- }).send();
1409
- if (!value) throw new Error(`Pump's Global account is missing at ${PUMP_GLOBAL}`);
1410
- const global = decodePumpGlobal(new Uint8Array(getBase64Encoder().encode(value.data[0])));
1411
- if (seed > global.initialRealTokenReserves) throw new Error(`the seed (${seed} tokens) is bigger than a fresh Pump curve holds (${global.initialRealTokenReserves} tokens); this table's top tier cannot be seeded on a new coin`);
1412
- }
1413
- /**
1414
- * The Pump buy accounts for the seed, built without reading the bonding curve.
1415
- *
1416
- * The curve does not exist yet — `create_v2` in the same transaction is what creates it — so
1417
- * `resolveVenue` cannot be used here. Everything the account list needs is known anyway: the
1418
- * creator vault follows from the `creator` argument that `create_v2` records on the curve, and the
1419
- * two fee recipients come from `Global`.
1420
- */
1421
- async function pumpSeedBuyAccounts(client, options) {
1422
- const { value } = await client.rpc.getAccountInfo(PUMP_GLOBAL, {
1423
- encoding: "base64",
1424
- commitment: "confirmed"
1425
- }).send();
1426
- if (!value) throw new Error(`Pump's Global account is missing at ${PUMP_GLOBAL}`);
1427
- const global = decodePumpGlobal(new Uint8Array(getBase64Encoder().encode(value.data[0])));
1428
- const recipients = [global.feeRecipient, ...global.feeRecipients];
1429
- const feeRecipient = recipients[options.feeRecipientIndex ?? randomIndex(recipients.length)];
1430
- const buyback = global.buybackFeeRecipients[options.buybackRecipientIndex ?? randomIndex(global.buybackFeeRecipients.length)];
1431
- if (!feeRecipient) throw new Error("Pump Global holds no fee recipient");
1432
- if (!buyback) throw new Error("Pump Global holds no buyback fee recipient");
1433
- return pumpBuyAccounts({
1434
- mint: options.mint,
1435
- user: options.user,
1436
- tokenProgram: PUMP_CREATE_TOKEN_PROGRAM_ADDRESS,
1437
- creator: options.user,
1438
- feeRecipient,
1439
- buybackFeeRecipient: buyback
1440
- });
1079
+ function assertSeedFitsCurve(seed) {
1080
+ if (seed > 793100000000000n) throw new Error(`the seed (${seed} base units) is bigger than the whole curve sells (${LAUNCH_TOTAL_BASE_SELL}); this table's top tier cannot be seeded on a new coin`);
1441
1081
  }
1442
- const randomIndex = (length) => {
1443
- if (length === 0) throw new Error("the list is empty");
1444
- return Math.floor(Math.random() * length);
1445
- };
1446
1082
  //#endregion
1447
1083
  //#region src/tx/draw.ts
1448
- /** A retry and an expiry are both small. The default budget is plenty. */
1449
1084
  const DRAW_COMPUTE_UNITS = 2e5;
1450
1085
  async function retryDraw(client, input) {
1451
- const retry = getRetryDrawInstruction({
1086
+ const ix = await getRetryDrawInstruction({
1452
1087
  payer: input.payer,
1453
1088
  pool: input.pool,
1454
1089
  draw: input.draw,
1455
1090
  identity: await vrfIdentityAddress(),
1456
1091
  maxVrfDebit: input.maxVrfDebit
1457
1092
  });
1458
- return await buildMessage(client, input.payer, [retry], {
1093
+ return await buildMessage(client, input.payer, [ix], {
1459
1094
  addressLookupTables: input.addressLookupTables,
1460
1095
  computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
1461
1096
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
@@ -1463,295 +1098,94 @@ async function retryDraw(client, input) {
1463
1098
  }
1464
1099
  async function expireDraw(client, input) {
1465
1100
  const draw = await fetchDraw(client, input.draw);
1466
- if (!draw) throw new Error(`no draw at ${input.draw}`);
1467
- if (draw.pool !== input.pool) throw new Error(`draw ${input.draw} does not belong to ${input.pool}`);
1101
+ if (!draw || draw.pool !== input.pool) throw new Error("draw does not belong to pool or was already delivered");
1468
1102
  const pool = await fetchPoolAt(client, input.pool);
1469
1103
  if (!pool) throw new Error(`no pool at ${input.pool}`);
1470
- const expire = getExpireDrawInstruction({
1104
+ const ix = getExpireDrawInstruction({
1471
1105
  pool: input.pool,
1472
1106
  draw: input.draw,
1473
1107
  purchaser: draw.purchaser,
1474
1108
  mint: pool.mint,
1475
1109
  vault: pool.vault,
1476
- userTokens: await associatedTokenAddress(draw.purchaser, pool.mint, pool.tokenProgram),
1477
- tokenProgram: pool.tokenProgram
1110
+ userTokens: await associatedTokenAddress(draw.purchaser, pool.mint)
1478
1111
  });
1479
- return await buildMessage(client, input.payer, [expire], {
1112
+ return await buildMessage(client, input.payer, [ix], {
1480
1113
  addressLookupTables: input.addressLookupTables,
1481
1114
  computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
1482
1115
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1483
1116
  });
1484
1117
  }
1485
- /**
1486
- * What a client may do to a draw right now.
1487
- *
1488
- * Reads the draw and the current slot, and reproduces the program's three retry conditions and the
1489
- * pending-status-plus-deadline expiry conditions. Showing a disabled button with a countdown beats
1490
- * sending a transaction that fails with `RetryTooSoon` or `NotPending`.
1491
- */
1492
- async function drawAvailability(client, draw) {
1493
- const record = await fetchDraw(client, draw);
1494
- if (!record) return null;
1495
- const slot = await client.rpc.getSlot({ commitment: "confirmed" }).send();
1496
- const now = BigInt(slot);
1497
- const retryAt = record.lastAttemptSlot + RETRY_SLOTS;
1498
- const expireAt = record.requestSlot + TIMEOUT_SLOTS;
1118
+ async function drawAvailability(client, address) {
1119
+ const draw = await fetchDraw(client, address);
1120
+ if (!draw) return null;
1121
+ const now = BigInt(await client.rpc.getSlot({ commitment: "confirmed" }).send());
1122
+ const retryAt = draw.lastAttemptSlot + RETRY_SLOTS;
1123
+ const expireAt = draw.requestSlot + TIMEOUT_SLOTS;
1499
1124
  const slotsUntilRetry = now >= retryAt ? 0n : retryAt - now;
1500
1125
  const slotsUntilExpiry = now >= expireAt ? 0n : expireAt - now;
1501
- const pending = record.status === 0;
1502
1126
  return {
1503
- status: record.status,
1504
- attempts: record.attempts,
1127
+ attempts: draw.attempts,
1505
1128
  slotsUntilRetry,
1506
1129
  slotsUntilExpiry,
1507
- canRetry: pending && slotsUntilRetry === 0n && record.attempts < 3 && slotsUntilExpiry > 0n,
1508
- canExpire: pending && slotsUntilExpiry === 0n
1130
+ canRetry: draw.attempts < 3 && slotsUntilRetry === 0n && slotsUntilExpiry > 0n,
1131
+ canExpire: slotsUntilExpiry === 0n
1509
1132
  };
1510
1133
  }
1511
1134
  //#endregion
1512
1135
  //#region src/tx/fundPrizes.ts
1513
- /**
1514
- * Donating prize inventory.
1515
- *
1516
- * `fund_prizes` moves tokens from any wallet into the pool's vault. It is a donation and it is
1517
- * irrevocable: there is no withdrawal instruction, and no authority can move vault tokens. The only
1518
- * way out of the vault is prize delivery.
1519
- *
1520
- * Funding raises what later packs can pay. It does not change the tiers, which are immutable, and
1521
- * it does not change any draw that is already frozen — `math.quote` is computed at purchase time
1522
- * and stored in the `Draw`.
1523
- */
1524
1136
  const FUND_COMPUTE_UNITS = 2e5;
1525
- /** A venue buy plus the vault transfer. The buy is the expensive half. */
1526
- const FUND_WITH_BUY_COMPUTE_UNITS = 4e5;
1527
1137
  async function fundPrizes(client, input) {
1528
- const { mint, funder, amount } = input;
1529
- if (amount <= 0n) throw new Error("amount must be positive");
1530
- const pool = await fetchPoolByMint(client, mint);
1531
- if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
1532
- return await buildMessage(client, funder, [getFundPrizesInstruction({
1533
- funder,
1534
- pool: await poolAddress(mint),
1535
- mint,
1536
- source: input.source ?? await associatedTokenAddress(funder.address, mint, pool.tokenProgram),
1138
+ if (input.amount <= 0n) throw new Error("amount must be positive");
1139
+ const pool = await fetchPoolByMint(client, input.mint);
1140
+ if (!pool) throw new Error(`no Gabox pool for mint ${input.mint}`);
1141
+ const ix = getFundPrizesInstruction({
1142
+ funder: input.funder,
1143
+ pool: await poolAddress(input.mint),
1144
+ mint: input.mint,
1145
+ source: input.source ?? await associatedTokenAddress(input.funder.address, input.mint),
1537
1146
  vault: pool.vault,
1538
- tokenProgram: pool.tokenProgram,
1539
- amount
1540
- })], {
1541
- addressLookupTables: input.addressLookupTables,
1542
- computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
1543
- ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1544
- });
1545
- }
1546
- /**
1547
- * Buy tokens at the venue with SOL and donate them to the vault, in one transaction.
1548
- *
1549
- * This is what a donor with SOL and no coins needs. `fundPrizes` moves tokens the donor already
1550
- * holds; this one buys them first. Both are irrevocable — there is no withdrawal instruction, and
1551
- * no authority can move vault tokens.
1552
- *
1553
- * Three instructions on the curve route: create the donor's token account if it is missing, buy,
1554
- * donate. Pump creates the account itself, but the idempotent instruction costs nothing when it
1555
- * already exists and it makes the transaction correct on its own terms. The PumpSwap route adds the
1556
- * WSOL create/fund/sync prefix, for the same reason `buyPack` does: PumpSwap spends WSOL.
1557
- */
1558
- async function fundPrizesWithBuy(client, input) {
1559
- const { mint, funder, tokens, maxQuoteIn } = input;
1560
- if (tokens <= 0n) throw new Error("tokens must be positive");
1561
- if (maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
1562
- const pool = await fetchPoolByMint(client, mint);
1563
- if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
1564
- const venue = await resolveVenue(client, {
1565
- mint,
1566
- user: funder.address,
1567
- ...input.venue ? { venue: input.venue } : {}
1147
+ amount: input.amount
1568
1148
  });
1569
- const source = await associatedTokenAddress(funder.address, mint, pool.tokenProgram);
1570
- return await buildMessage(client, funder, [
1571
- ...venue.kind === "pumpswap" ? wsolPreparation$1(funder, input.wrapLamports ?? maxQuoteIn) : [],
1572
- getCreateAssociatedTokenIdempotentInstruction({
1573
- payer: funder,
1574
- ata: source,
1575
- owner: funder.address,
1576
- mint,
1577
- tokenProgram: pool.tokenProgram
1578
- }),
1579
- venueBuyInstruction({
1580
- venue,
1581
- user: funder,
1582
- tokens,
1583
- maxQuoteIn
1584
- }),
1585
- getFundPrizesInstruction({
1586
- funder,
1587
- pool: await poolAddress(mint),
1588
- mint,
1589
- source,
1590
- vault: pool.vault,
1591
- tokenProgram: pool.tokenProgram,
1592
- amount: tokens
1593
- })
1594
- ], {
1149
+ return await buildMessage(client, input.funder, [ix], {
1595
1150
  addressLookupTables: input.addressLookupTables,
1596
- computeUnitLimit: input.computeUnitLimit ?? FUND_WITH_BUY_COMPUTE_UNITS,
1151
+ computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
1597
1152
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1598
1153
  });
1599
1154
  }
1600
- /** Create the donor's WSOL account if needed, fund it, and sync its balance. See `buyPack.ts`. */
1601
- function wsolPreparation$1(funder, lamports) {
1602
- const wsol = wsolAccountFor(funder.address);
1603
- return [
1604
- getCreateAssociatedTokenIdempotentInstruction({
1605
- payer: funder,
1606
- ata: wsol,
1607
- owner: funder.address,
1608
- mint: WSOL_MINT,
1609
- tokenProgram: TOKEN_PROGRAM_ADDRESS$1
1610
- }),
1611
- getTransferSolInstruction({
1612
- source: funder,
1613
- destination: wsol,
1614
- amount: lamports
1615
- }),
1616
- getSyncNativeInstruction({ account: wsol })
1617
- ];
1618
- }
1619
1155
  //#endregion
1620
1156
  //#region src/tx/redeem.ts
1621
- /**
1622
- * Selling an automatically delivered prize, plus settlement helpers for legacy resolve-only draws.
1623
- *
1624
- * Every sale here goes through the program (`sell_tokens` or `sell_prize`), which does the venue
1625
- * CPI and then pays the protocol 1% of what the sale returned. On Pump the proceeds and the fee are
1626
- * native SOL. On PumpSwap both are WSOL: the proceeds land in the seller's WSOL account and the fee
1627
- * moves from there to the collector's WSOL account. Add an unwrap afterwards if the seller wants
1628
- * lamports.
1629
- *
1630
- * `minQuoteOutput` is the venue's own floor, checked before the fee. The seller keeps 99% of
1631
- * whatever the venue paid above it.
1632
- */
1633
- /**
1634
- * Sell a prize that the VRF callback already delivered to the wallet, through `sell_tokens`.
1635
- *
1636
- * The program needs the coin's pool: it keeps the vault out of the venue's account list. A mint
1637
- * with no pool cannot be sold this way; use a plain venue trade for that.
1638
- */
1639
1157
  async function sellTokens(client, input) {
1640
- const { mint, seller, amount, minQuoteOutput } = input;
1641
- if (amount <= 0n) throw new Error("amount must be positive");
1642
- if (minQuoteOutput <= 0n) throw new Error("minQuoteOutput must be positive");
1643
- const pool = await fetchPoolByMint(client, mint);
1644
- if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
1158
+ if (input.amount <= 0n || input.minQuoteOutput <= 0n) throw new Error("amount and minQuoteOutput must be positive");
1159
+ if (input.maxNativeDebit < 0n) throw new Error("maxNativeDebit must not be negative");
1160
+ const pool = await fetchPoolByMint(client, input.mint);
1161
+ if (!pool) throw new Error(`no Gabox pool for mint ${input.mint}`);
1645
1162
  const venue = await resolveVenue(client, {
1646
- mint,
1647
- user: seller.address,
1163
+ mint: input.mint,
1164
+ user: input.seller.address,
1648
1165
  ...input.venue ? { venue: input.venue } : {}
1649
1166
  });
1650
- const sell = getSellTokensInstruction({
1651
- seller,
1652
- pool: await poolAddress(mint),
1653
- mint,
1654
- userTokens: await associatedTokenAddress(seller.address, mint, pool.tokenProgram),
1167
+ const sell = await getSellTokensInstructionAsync({
1168
+ seller: input.seller,
1169
+ pool: await poolAddress(input.mint),
1170
+ mint: input.mint,
1171
+ quoteMint: pool.quoteMint,
1655
1172
  venue: venue.program,
1656
- feeCollectorWsol: await feeCollectorWsolAddress(),
1657
- tokenProgram: pool.tokenProgram,
1658
- amount,
1659
- minQuoteOutput
1660
- });
1661
- return await buildMessage(client, seller, [...wsolPreparation(seller, venue), withRemainingAccounts(sell, venue.sellAccounts)], {
1662
- addressLookupTables: input.addressLookupTables,
1663
- computeUnitLimit: input.computeUnitLimit ?? 3e5,
1664
- ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1173
+ amount: input.amount,
1174
+ minQuoteOutput: input.minQuoteOutput,
1175
+ maxNativeDebit: input.maxNativeDebit
1665
1176
  });
1666
- }
1667
- /** Transfer a legacy resolve-only award into the purchaser's token account and close the draw. */
1668
- async function claimPrize(client, input) {
1669
- const { mint, purchaser, draw } = input;
1670
- const pool = await fetchPoolByMint(client, mint);
1671
- if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
1672
- return await buildMessage(client, purchaser, [getClaimPrizeInstruction({
1673
- purchaser,
1674
- pool: await poolAddress(mint),
1675
- draw,
1676
- mint,
1677
- vault: pool.vault,
1678
- userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
1679
- tokenProgram: pool.tokenProgram
1680
- })], {
1681
- addressLookupTables: input.addressLookupTables,
1682
- computeUnitLimit: input.computeUnitLimit ?? 3e5,
1683
- ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1684
- });
1685
- }
1686
- /**
1687
- * Claim and sell a legacy resolve-only draw in one transaction.
1688
- *
1689
- * The award is read off the draw so the caller can price the sale before signing. A draw that has
1690
- * not resolved has no award yet, and this throws rather than building a sale of zero tokens.
1691
- */
1692
- async function sellPrize(client, input) {
1693
- const { mint, purchaser, draw, minQuoteOutput } = input;
1694
- if (minQuoteOutput <= 0n) throw new Error("minQuoteOutput must be positive");
1695
- const pool = await fetchPoolByMint(client, mint);
1696
- if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
1697
- const record = await fetchDraw(client, draw);
1698
- if (!record) throw new Error(`no draw at ${draw} — it may already be redeemed`);
1699
- if (record.amount === 0n) throw new Error(`draw ${draw} has no award yet; it has not resolved`);
1700
- const venue = await resolveVenue(client, {
1701
- mint,
1702
- user: purchaser.address,
1703
- ...input.venue ? { venue: input.venue } : {}
1704
- });
1705
- const sell = getSellPrizeInstruction({
1706
- purchaser,
1707
- pool: await poolAddress(mint),
1708
- draw,
1709
- mint,
1710
- vault: pool.vault,
1711
- userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
1712
- tokenProgram: pool.tokenProgram,
1713
- venue: venue.program,
1714
- feeCollectorWsol: await feeCollectorWsolAddress(),
1715
- minQuoteOutput
1716
- });
1717
- return await buildMessage(client, purchaser, [...wsolPreparation(purchaser, venue), withRemainingAccounts(sell, venue.sellAccounts)], {
1177
+ const wsol = await fundWsol(input.seller, 0n);
1178
+ const instructions = [
1179
+ ...wsol.instructions,
1180
+ withRemainingAccounts(sell, venue.sellAccounts),
1181
+ unwrapWsol(input.seller, wsol.account)
1182
+ ];
1183
+ return await buildMessage(client, input.seller, instructions, {
1718
1184
  addressLookupTables: input.addressLookupTables,
1719
- computeUnitLimit: input.computeUnitLimit ?? 3e5,
1185
+ computeUnitLimit: input.computeUnitLimit ?? 135e3,
1720
1186
  ...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
1721
1187
  });
1722
1188
  }
1723
- /**
1724
- * On PumpSwap the proceeds land in the seller's WSOL account, so it has to exist before the sale.
1725
- * Pump's own SDK does the same before its sells. The instruction is idempotent and the account is
1726
- * the seller's own; gabox never touches it. Pump pays native SOL, so the curve route adds nothing.
1727
- */
1728
- function wsolPreparation(seller, venue) {
1729
- if (venue.kind !== "pumpswap") return [];
1730
- return [getCreateAssociatedTokenIdempotentInstruction({
1731
- payer: seller,
1732
- ata: wsolAccountFor(seller.address),
1733
- owner: seller.address,
1734
- mint: WSOL_MINT,
1735
- tokenProgram: TOKEN_PROGRAM_ADDRESS$1
1736
- })];
1737
- }
1738
- /**
1739
- * Quote the award held by a legacy open draw before the seller signs a floor.
1740
- *
1741
- * Read the draw, ask the venue, and subtract your own slippage tolerance to get `minQuoteOutput`.
1742
- */
1743
- async function quoteSellPrize(client, mint, draw, user) {
1744
- const record = await fetchDraw(client, draw);
1745
- if (!record) throw new Error(`no draw at ${draw}`);
1746
- const venue = await resolveVenue(client, {
1747
- mint,
1748
- user
1749
- });
1750
- return {
1751
- award: record.amount,
1752
- grossOutput: venue.quoteSell(record.amount)
1753
- };
1754
- }
1755
1189
  //#endregion
1756
1190
  //#region src/vrf.ts
1757
1191
  /**
@@ -1770,6 +1204,6 @@ async function oracleAccounts() {
1770
1204
  };
1771
1205
  }
1772
1206
  //#endregion
1773
- export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, BIND_REFERRER_COMPUTE_UNITS, BPS, BUY_PACK_COMPUTE_UNITS, CLUSTER_ENDPOINTS, COMPUTE_BUDGET_PROGRAM_ADDRESS, CREATE_MACHINE_COMPUTE_UNITS, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_TIERS, DEVNET_ADDRESS_LOOKUP_TABLES, DEVNET_HTTP, DEVNET_LOOKUP_TABLE_ADDRESS, DEVNET_LOOKUP_TABLE_ADDRESSES, DEVNET_WS, DRAW_DISCRIMINATOR, DRAW_POOL_OFFSET, DRAW_PURCHASER_OFFSET, GABOX_PROGRAM_ID, GaboxMathError, IDENTITY_SEED, INSTRUCTIONS_SYSVAR, MAX_ATTEMPTS, MAX_COMPUTE_UNIT_LIMIT, MAX_FEE_BPS, MAYHEM_PROGRAM_ADDRESS, PACK_TOKENS, POOL_CREATOR_OFFSET, POOL_DISCRIMINATOR, POOL_MINT_OFFSET, PROTOCOL_FEE_BPS, PROTOCOL_FEE_COLLECTOR, PUMP_FEE_PROGRAM_ADDRESS, PUMP_PROGRAM_ADDRESS, PUMP_SWAP_PROGRAM_ADDRESS, REDEEM_COMPUTE_UNITS, REFERRAL_FEE_BPS, REFERRAL_LINK_REFERRER_OFFSET, RETRY_SLOTS, SLOT_HASHES_SYSVAR, SYSTEM_PROGRAM_ADDRESS, TICKETS, TIERS, TIMEOUT_SLOTS, TOKEN_2022_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, VRF_DEFAULT_QUEUE, VRF_PROGRAM_ADDRESS, WSOL_MINT, assertClusterUrl, associatedTokenAddress, averageMultiplierBps, bindReferrer, buildMessage, buyPack, choose, claimPrize, claimReferral, clusterNamedBy, computeBudgetInstructions, createClient, createMachine, decodeDraw, decodeEvent, decodeEvents, decodePool, defaultAddressLookupTables, drawAddress, drawAvailability, expireDraw, feeCollectorWsolAddress, fetchDraw, fetchEvents, fetchPoolAt, fetchPoolByMint, fetchPoolInventory, fetchReferralReward, fetchVaultBalance, findDrawPda, findIdentityPda, findPoolPda, findReferralLinkPda, fundPrizes, fundPrizesWithBuy, generated_exports as generated, getOffer, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, listDraws, listDrawsByPool, listDrawsByPurchaser, listPools, listReferralLinksByReferrer, maxMultiplierBps, offerFor, offerFromState, oracleAccounts, poolAddress, pump_exports as pump, pumpSeedBuyAccounts, quote, quoteSellPrize, referralAddress, referralLinkAddress, resolveReferral, resolveReservation, retryDraw, scopedVrfIdentityAddress, seedCostEstimate, seedShortfall, seedTokens, sellPrize, sellTokens, share, tierAmount, tiersOf, uncappedMaximum, validatePack, validateTiers, vaultAddress, vrfIdentityAddress, watchDraw, websocketUrlFor, withRemainingAccounts };
1207
+ export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, BPS, BUY_PACK_COMPUTE_UNITS, CLAIM_COMPUTE_UNITS, CLUSTER_ENDPOINTS, COMPUTE_BUDGET_PROGRAM_ADDRESS, CREATE_MACHINE_COMPUTE_UNITS, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_TIERS, DEVNET_ADDRESS_LOOKUP_TABLES, DEVNET_HTTP, DEVNET_LOOKUP_TABLE_ADDRESS, DEVNET_LOOKUP_TABLE_ADDRESSES, DEVNET_WS, DRAW_DISCRIMINATOR, DRAW_POOL_OFFSET, DRAW_PURCHASER_OFFSET, GABOX_PROGRAM_ID, GaboxMathError, IDENTITY_SEED, INSTRUCTIONS_SYSVAR, LAUNCH_DECIMALS, MAX_ATTEMPTS, MAX_COMPUTE_UNIT_LIMIT, MAX_MULTIPLIER_BPS, METAPLEX_PROGRAM_ADDRESS, MIN_SEED_MULTIPLIER_BPS, PACK_TOKENS, PLATFORM_ADMIN, POOL_CREATOR_OFFSET, POOL_DISCRIMINATOR, POOL_MINT_OFFSET, REDEEM_COMPUTE_UNITS, RETRY_SLOTS, SLOT_HASHES_SYSVAR, SYSTEM_PROGRAM_ADDRESS, TICKETS, TIERS, TIMEOUT_SLOTS, TOKEN_PROGRAM_ADDRESS, VRF_DEFAULT_QUEUE, VRF_PROGRAM_ADDRESS, WALLET_ACTIVITY_DISCRIMINATOR, WSOL_MINT, activityAddress, assertClusterUrl, associatedTokenAddress, averageMultiplierBps, buildMessage, buyPack, choose, clusterNamedBy, computeBudgetInstructions, createClient, createMachine, decodeDraw, decodeEvent, decodeEvents, decodePool, decodeWalletActivity, defaultAddressLookupTables, drawAddress, drawAvailability, expireDraw, fetchDraw, fetchEvents, fetchPoolAt, fetchPoolByMint, fetchPoolInventory, fetchVaultBalance, fetchWalletActivity, findActivityPda, findDrawPda, findIdentityPda, findPoolPda, findResolvedDraw, fundPrizes, fundWsol, generated_exports as generated, getOffer, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, listDraws, listDrawsByPool, listDrawsByPurchaser, listPools, maxMultiplierBps, offerFromState, oracleAccounts, poolAddress, quote, quotePool, raydium_exports as raydium, resolveReservation, retryDraw, scopedVrfIdentityAddress, seedCostEstimate, seedShortfall, seedTokens, sellTokens, tierAmount, tiersOf, uncappedMaximum, unwrapWsol, validatePack, validateTiers, vaultAddress, vrfIdentityAddress, websocketUrlFor, withRemainingAccounts };
1774
1208
 
1775
1209
  //# sourceMappingURL=index.js.map