@gabox-labs/sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,7 +3,29 @@
3
3
  All notable changes to `@gabox-labs/sdk`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  and the project uses [Semantic Versioning](https://semver.org/) with the `0.x` rule from `CONTRIBUTING.md`.
5
5
 
6
- ## [Unreleased]
6
+ ## [0.2.0] - 2026-09-13
7
+
8
+ ### Breaking
9
+
10
+ - `createMachine` now accepts an optional `tiers` table instead of required `riskProfile` and
11
+ `jackpotBps` inputs. `seedCostEstimate` now takes the same optional table. Both default to
12
+ `DEFAULT_TIERS`, the Gabox app's 20× top-prize table.
13
+ - Removed `jackpotTiers` and `RiskProfile`. Pass an explicit validated eight-row `Tier[]` to use a
14
+ different prize table.
15
+
16
+ ### Security
17
+
18
+ - `expireDraw` is documented and generated from the program revision that only permits expiry of
19
+ an expired `Pending` draw. A `Ready` draw is rejected with `NotPending`, preserving its resolved
20
+ award for normal delivery.
21
+
22
+ ### Changed
23
+
24
+ - Code generation validates the complete program IDL and Pump ABI before replacing generated code
25
+ or committed IDL snapshots. A failed ABI cross-check now leaves the SDK unchanged.
26
+ - The matching devnet program upgrade is verified at slot `497869792`. Its first 586,304 deployed
27
+ bytes match the reviewed SBF SHA-256 `fdbbed4161a9f2f22ce02547dc3e9ec67a22020ab58be9403d04e31464dadf89`;
28
+ its 595,976-byte allocation has only zero padding beyond that build.
7
29
 
8
30
  ## [0.1.1] - 2026-09-11
9
31
 
package/README.md CHANGED
@@ -35,8 +35,8 @@ one pack, each with a number of tickets out of 65,536. Verifiable randomness (Ma
35
35
  one prize, and the tokens are sent straight to the buyer's wallet. The buyer signs once, at
36
36
  purchase. Keeping the prize needs nothing else. Selling it later is one more transaction.
37
37
 
38
- The **creator** chooses the jackpot size (for example 5×) and a risk profile. The creator buys the
39
- tokens that back the jackpot at creation, and earns a fee on every pack.
38
+ The **creator** chooses an eight-row prize table, or uses `DEFAULT_TIERS`. The creator buys the
39
+ tokens that back the table's top prize at creation, and earns a fee on every pack.
40
40
 
41
41
  ---
42
42
 
@@ -162,11 +162,11 @@ per unit), and `addressLookupTables`. The defaults are generous.
162
162
  ### Create a machine
163
163
 
164
164
  ```ts
165
- import { createMachine, seedCostEstimate } from '@gabox-labs/sdk';
165
+ import { createMachine, DEFAULT_TIERS, seedCostEstimate } from '@gabox-labs/sdk';
166
166
  import { generateKeyPairSigner } from '@solana/kit';
167
167
 
168
168
  const mintKeypair = await generateKeyPairSigner();
169
- const seed = await seedCostEstimate(gabox, 50_000, 'balanced');
169
+ const seed = await seedCostEstimate(gabox); // DEFAULT_TIERS
170
170
 
171
171
  const message = await createMachine(gabox, {
172
172
  creator, // TransactionSigner. Pays for everything.
@@ -175,8 +175,7 @@ const message = await createMachine(gabox, {
175
175
  symbol: 'CAT',
176
176
  uri: 'https://…/metadata.json',
177
177
  feeBps: 100, // the creator's fee per pack, 0–100 bps (max 1%)
178
- riskProfile: 'balanced', // 'conservative' | 'balanced' | 'jackpot'
179
- jackpotBps: 50_000, // the top prize: 5× one pack
178
+ tiers: DEFAULT_TIERS, // optional; the default is a 20× top prize
180
179
  maxSeedLamports: (seed.lamports * 105n) / 100n,
181
180
  });
182
181
  ```
@@ -184,15 +183,16 @@ const message = await createMachine(gabox, {
184
183
  One transaction, two signers: the creator and the new mint. The creator pays for the coin, the
185
184
  pool, and the **seed**.
186
185
 
187
- The seed is the inventory that backs the jackpot. The first pack must be able to pay the top prize
188
- in full. A jackpot needs four extra packs of tokens in the vault; a 10× jackpot needs nine. A 1×
189
- jackpot needs no seed. `seedCostEstimate` tells you what that costs on a fresh Pump curve, in
190
- lamports and tokens. `maxSeedLamports` is the creator's slippage cap on that buy.
186
+ The seed is the inventory that backs the top prize. The first pack must be able to pay that prize in
187
+ full. `DEFAULT_TIERS` has 75%, 20%, 4%, and 1% odds at 0.52×, 1.2×, 3×, and 20× respectively, and
188
+ needs nineteen extra packs in the vault. `seedCostEstimate` tells you what that costs on a fresh
189
+ Pump curve, in lamports and tokens. `maxSeedLamports` is the creator's slippage cap on that buy.
191
190
 
192
- - `jackpotBps` is the largest prize, in basis points of one pack. `10_000` is 1×. `50_000` is 5×.
193
- - `riskProfile` decides how the odds are spread. Every profile returns 95% of a pack's value on
194
- average. `conservative` keeps more of it in the common prize; `jackpot` moves more into the rare
195
- ones. See `jackpotTiers(jackpotBps, profile)` to preview the table before creating.
191
+ `tiers` is optional and defaults to `DEFAULT_TIERS`, the table the Gabox app creates pools with. A
192
+ creator may instead supply an eight-row `Tier[]`. The SDK validates it locally and the program
193
+ validates it again: ticket counts must total 65,536, unused rows must be all-zero, expected payout
194
+ cannot exceed one pack, each live tier must pay at least one token, and the largest prize must be at
195
+ least one pack. `seedCostEstimate(gabox, tiers)` previews the exact custom table before creating.
196
196
 
197
197
  ### Price a pack
198
198
 
@@ -350,9 +350,10 @@ if (state?.canExpire) await expireDraw(gabox, { payer, pool, draw });
350
350
 
351
351
  - `retryDraw` asks for randomness again. Allowed 300 slots after the last attempt, up to 3 attempts
352
352
  in total. The payer covers the request fee, capped by `maxVrfDebit`.
353
- - `expireDraw` settles a draw that never received randomness, 216,000 slots (about one day) after
354
- the purchase. It pays the **smallest prize** on the draw's table. It is not a refund. Tell buyers
355
- this before they pay.
353
+ - `expireDraw` settles a **pending** draw that never received randomness, 216,000 slots (about one
354
+ day) after the purchase. It pays the **smallest prize** on the draw's table. It is not a refund.
355
+ It rejects a `Ready` draw: randomness has already resolved that outcome and the normal delivery
356
+ path must pay it. Tell buyers this before they pay.
356
357
 
357
358
  Use `drawAvailability` to show a countdown instead of sending a transaction that fails.
358
359
 
@@ -429,6 +430,7 @@ The ones a UI will meet:
429
430
  | `RetryTooSoon` | `retryDraw` before 300 slots passed |
430
431
  | `RetryUnavailable` | `retryDraw` after 3 attempts, or on a draw that is no longer pending |
431
432
  | `NotExpired` | `expireDraw` before the deadline |
433
+ | `NotPending` | `expireDraw` on a draw whose randomness has already resolved |
432
434
  | `InvalidReferral` | a self-referral, or a link that does not match |
433
435
  | `ZeroAmount` | a zero `amount` or a zero `minQuoteOutput` |
434
436
 
@@ -493,10 +495,10 @@ This repository is agent-ready.
493
495
  ## Status
494
496
 
495
497
  - **Devnet only.** Mainnet is not live.
496
- - The devnet program is being upgraded to the build this SDK targets. Until that upgrade lands,
497
- transactions built by this SDK do not match the deployed program, and `fetchPoolByMint` reports
498
- machines from the previous build as an "older devnet account layout". Watch the
499
- [changelog](CHANGELOG.md) for the release that marks the upgrade done.
498
+ - The devnet program revision this SDK targets is live and verified at slot `497869792`; see the
499
+ [0.2.0 changelog](CHANGELOG.md).
500
+ Machines from the previous account layout remain incompatible: `fetchPoolByMint` excludes them,
501
+ and direct reads report an "older devnet account layout" migration requirement.
500
502
  - The Pump bonding-curve route has an end-to-end test on devnet (`GABOX_DEVNET_E2E=1`). The
501
503
  PumpSwap route (coins that graduated off the curve) has unit coverage only.
502
504
  - The API is `0.x`. Breaking changes bump the minor version and are listed in the changelog.
package/dist/index.d.ts CHANGED
@@ -44,8 +44,15 @@ export declare class GaboxMathError extends Error {
44
44
  readonly code: string;
45
45
  constructor(code: string, message: string);
46
46
  }
47
- /** Creator-facing presets. The creator picks the jackpot size; the profile chooses its odds. */
48
- export type RiskProfile = "conservative" | "balanced" | "jackpot";
47
+ /**
48
+ * The default prize table. The program does not enforce this table: it accepts any table that
49
+ * passes `validateTiers`. This is only the table the Gabox app creates its pools with, and the
50
+ * starting point for a client that has no reason to pick another one.
51
+ *
52
+ * Common 75% at 0.52x, Rare 20% at 1.2x, Epic 4% at 3x, Mythic 1% at 20x. Expected payout is
53
+ * 0.9499x of a pack, so the seed is 19 packs.
54
+ */
55
+ export declare const DEFAULT_TIERS: readonly Readonly<Tier>[];
49
56
  /** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
50
57
  export declare function tierAmount(base: bigint, multiplierBps: number): bigint;
51
58
  /**
@@ -57,14 +64,14 @@ export declare function tierAmount(base: bigint, multiplierBps: number): bigint;
57
64
  * - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
58
65
  * tokens than a pack buys. This bounds tokens, not cash value.
59
66
  */
60
- export declare function validateTiers(tiers: readonly Tier[]): void;
67
+ export declare function validateTiers(tiers: readonly Readonly<Tier>[]): void;
61
68
  /**
62
69
  * `math::validate_pack`. Every ticketed tier must pay at least one token for a pack of this size.
63
70
  *
64
71
  * The program checks this once at creation. At `PACK_TOKENS` no sane table fails it; it exists so
65
72
  * a table cannot sell a ticket that can only win zero.
66
73
  */
67
- export declare function validatePack(packTokens: bigint, tiers: readonly Tier[]): void;
74
+ export declare function validatePack(packTokens: bigint, tiers: readonly Readonly<Tier>[]): void;
68
75
  /**
69
76
  * `math::seed_tokens`. The seed a table needs, in tokens.
70
77
  *
@@ -79,26 +86,13 @@ export declare function validatePack(packTokens: bigint, tiers: readonly Tier[])
79
86
  * `initialize_pool` computes this number itself and buys exactly that many tokens on the curve.
80
87
  * The creator only signs a maximum SOL cost.
81
88
  */
82
- export declare function seedTokens(packTokens: bigint, tiers: readonly Tier[]): bigint;
83
- /**
84
- * Derive a fully valid four-outcome rarity ladder from a jackpot multiplier and a risk preset.
85
- *
86
- * Mythic is exactly `jackpotBps`. Rare and Epic interpolate between sub-1x launch values
87
- * and Mythic as the jackpot grows, so the four payouts remain strictly ordered even at 1x.
88
- * The profile assigns fixed slices of one pack's EV budget to the three higher rarities. Whatever
89
- * remains of the 95% target becomes Common. Integer rounding is always downward, so the result
90
- * cannot cross the program's 100% expected-value ceiling.
91
- *
92
- * The seed this table needs is `seedTokens(packTokens, tiers)`: `(jackpotBps - 10_000)` bps of
93
- * one pack.
94
- */
95
- export declare function jackpotTiers(jackpotBps: number, profile: RiskProfile): Tier[];
89
+ export declare function seedTokens(packTokens: bigint, tiers: readonly Readonly<Tier>[]): bigint;
96
90
  /**
97
91
  * `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
98
92
  *
99
93
  * At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
100
94
  */
101
- export declare function uncappedMaximum(base: bigint, tiers: readonly Tier[]): bigint;
95
+ export declare function uncappedMaximum(base: bigint, tiers: readonly Readonly<Tier>[]): bigint;
102
96
  /**
103
97
  * `math::quote`. The offer a pack of `base` tokens would freeze right now. `base` is always
104
98
  * `pool.packTokens`; the parameter stays general so the vectors can use small numbers.
@@ -110,7 +104,7 @@ export declare function uncappedMaximum(base: bigint, tiers: readonly Tier[]): b
110
104
  * Every amount is capped at what is actually available. A tier that rounds to zero tokens is an
111
105
  * error: the pool must not sell a ticket that can only win nothing.
112
106
  */
113
- export declare function quote(base: bigint, tiers: readonly Tier[], inventory: bigint, reserved: bigint): Offer;
107
+ export declare function quote(base: bigint, tiers: readonly Readonly<Tier>[], inventory: bigint, reserved: bigint): Offer;
114
108
  /**
115
109
  * `math::choose`. Which prize a 16-bit ticket wins.
116
110
  *
@@ -121,14 +115,14 @@ export declare function choose(prizes: readonly Prize[], ticket: number): bigint
121
115
  /** `math::resolve_reservation`. Release the unwon part of a maximum, keep the award reserved. */
122
116
  export declare function resolveReservation(reserved: bigint, maximum: bigint, award: bigint): bigint;
123
117
  /** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
124
- export declare function maxMultiplierBps(tiers: readonly Tier[]): number;
118
+ export declare function maxMultiplierBps(tiers: readonly Readonly<Tier>[]): number;
125
119
  /**
126
120
  * The expected multiplier over all 65,536 tickets, in basis points. Rounded down.
127
121
  *
128
122
  * `validateTiers` caps this at 10,000. A table at 9,800 keeps 2% of every pack's tokens in the
129
123
  * vault on average, which is what lets a pool survive a run of top-tier wins.
130
124
  */
131
- export declare function averageMultiplierBps(tiers: readonly Tier[]): number;
125
+ export declare function averageMultiplierBps(tiers: readonly Readonly<Tier>[]): number;
132
126
  //#endregion
133
127
  //#region src/accounts.d.ts
134
128
  /** `Draw.pool` sits straight after the discriminator. */
@@ -593,13 +587,12 @@ export type CreateMachineInput = {
593
587
  uri: string;
594
588
  /** The creator's fee per pack, in bps of what the venue charges. `0` to `MAX_FEE_BPS`. Immutable. */
595
589
  feeBps: number;
596
- /** Controls how much probability moves from Common into Rare, Epic, and Mythic. */
597
- riskProfile: RiskProfile;
598
590
  /**
599
- * The jackpot: the largest tier's multiplier, in bps. `10_000` is 1x and needs no seed;
600
- * `50_000` is 5x and seeds four packs of tokens. Immutable.
591
+ * The prize table. Immutable once the pool exists. Must pass `validateTiers` and
592
+ * `validatePack(PACK_TOKENS, tiers)`; the program checks both again on-chain. Defaults to
593
+ * `DEFAULT_TIERS`, the table the Gabox app uses.
601
594
  */
602
- jackpotBps: number;
595
+ tiers?: readonly Readonly<Tier>[];
603
596
  /**
604
597
  * The creator's slippage cap on the seed buy, in lamports. Pump fails the buy above it.
605
598
  * Take `seedCostEstimate` and add a margin. Ignored for a 1x jackpot, which buys nothing.
@@ -619,14 +612,17 @@ export type CreateMachineInput = {
619
612
  */
620
613
  export declare function createMachine(client: GaboxClient, input: CreateMachineInput): Promise<GaboxTransactionMessage>;
621
614
  /**
622
- * What the seed for this jackpot costs, fees included, and how many tokens it is.
615
+ * What the seed for this table costs, fees included, and how many tokens it is.
623
616
  *
624
617
  * The coin does not exist yet, so the price is Pump's default new curve. Nothing else trades on
625
618
  * it before `initialize_pool` runs in the same transaction, so this is exact up to a change in
626
619
  * Pump's fee settings between the read and the send. Add a small margin for `maxSeedLamports`.
620
+ *
621
+ * Defaults to `DEFAULT_TIERS`. Throws if `tiers` fails `validateTiers`/`validatePack`, or if the
622
+ * seed is bigger than a fresh Pump curve can sell in one buy.
627
623
  */
628
- export declare function seedCostEstimate(client: GaboxClient, jackpotBps: number, riskProfile: RiskProfile): Promise<{
629
- tiers: Tier[];
624
+ export declare function seedCostEstimate(client: GaboxClient, tiers?: readonly Readonly<Tier>[]): Promise<{
625
+ tiers: readonly Tier[];
630
626
  seedTokens: bigint;
631
627
  lamports: bigint;
632
628
  }>;
@@ -667,19 +663,19 @@ export type DrawAvailability = {
667
663
  attempts: number;
668
664
  /** Slots remaining before a retry is allowed. `0` when it is allowed now. */
669
665
  slotsUntilRetry: bigint;
670
- /** Slots remaining before the draw can be expired. `0` when it can be expired now. */
666
+ /** Slots remaining before an unanswered draw reaches its expiry deadline. */
671
667
  slotsUntilExpiry: bigint;
672
668
  /** All three conditions the program checks for `retry_draw`, together. */
673
669
  canRetry: boolean;
674
- /** The one condition `expire_draw` checks. */
670
+ /** True only when the draw is still `Pending` and its expiry deadline passed. */
675
671
  canExpire: boolean;
676
672
  };
677
673
  /**
678
674
  * What a client may do to a draw right now.
679
675
  *
680
- * Reads the draw and the current slot, and reproduces the program's three retry conditions and its
681
- * one expiry condition. Showing a disabled button with a countdown beats sending a transaction that
682
- * fails with `RetryTooSoon`.
676
+ * Reads the draw and the current slot, and reproduces the program's three retry conditions and the
677
+ * pending-status-plus-deadline expiry conditions. Showing a disabled button with a countdown beats
678
+ * sending a transaction that fails with `RetryTooSoon` or `NotPending`.
683
679
  */
684
680
  export declare function drawAvailability(client: GaboxClient, draw: Address): Promise<DrawAvailability | null>;
685
681
  //#endregion
package/dist/index.js CHANGED
@@ -36,45 +36,61 @@ var GaboxMathError = class extends Error {
36
36
  }
37
37
  };
38
38
  const U64_MAX = (1n << 64n) - 1n;
39
- const U32_MAX = (1n << 32n) - 1n;
39
+ const U32_MAX = 4294967295;
40
40
  /**
41
- * All profiles target a 95% token return. These are slices of one pack's expected-value budget
42
- * assigned to Rare, Epic, and Mythic respectively. A riskier profile moves more of that budget
43
- * out of Common and into every higher rarity; the ticket counts still decrease as rarity rises.
41
+ * The default prize table. The program does not enforce this table: it accepts any table that
42
+ * passes `validateTiers`. This is only the table the Gabox app creates its pools with, and the
43
+ * starting point for a client that has no reason to pick another one.
44
+ *
45
+ * Common 75% at 0.52x, Rare 20% at 1.2x, Epic 4% at 3x, Mythic 1% at 20x. Expected payout is
46
+ * 0.9499x of a pack, so the seed is 19 packs.
44
47
  */
45
- const RISK_PROFILES = {
46
- conservative: {
47
- rarityEvBps: [
48
- 1000n,
49
- 600n,
50
- 300n
51
- ],
52
- targetEvBps: 9500n
53
- },
54
- balanced: {
55
- rarityEvBps: [
56
- 1400n,
57
- 1000n,
58
- 700n
59
- ],
60
- targetEvBps: 9500n
61
- },
62
- jackpot: {
63
- rarityEvBps: [
64
- 1800n,
65
- 1500n,
66
- 1200n
67
- ],
68
- targetEvBps: 9500n
69
- }
70
- };
48
+ const DEFAULT_TIERS = Object.freeze([
49
+ Object.freeze({
50
+ multiplierBps: 5201,
51
+ tickets: 49153
52
+ }),
53
+ Object.freeze({
54
+ multiplierBps: 12e3,
55
+ tickets: 13107
56
+ }),
57
+ Object.freeze({
58
+ multiplierBps: 3e4,
59
+ tickets: 2621
60
+ }),
61
+ Object.freeze({
62
+ multiplierBps: 2e5,
63
+ tickets: 655
64
+ }),
65
+ Object.freeze({
66
+ multiplierBps: 0,
67
+ tickets: 0
68
+ }),
69
+ Object.freeze({
70
+ multiplierBps: 0,
71
+ tickets: 0
72
+ }),
73
+ Object.freeze({
74
+ multiplierBps: 0,
75
+ tickets: 0
76
+ }),
77
+ Object.freeze({
78
+ multiplierBps: 0,
79
+ tickets: 0
80
+ })
81
+ ]);
71
82
  function checkedU64(value, what) {
72
83
  if (value < 0n || value > U64_MAX) throw new GaboxMathError("Arithmetic", `${what} does not fit in u64`);
73
84
  return value;
74
85
  }
86
+ /** Tier fields encode as program `u32`s. Reject JavaScript-only values before `BigInt` or codecs. */
87
+ function checkedU32(value, what) {
88
+ if (!Number.isInteger(value) || value < 0 || value > U32_MAX) throw new GaboxMathError("InvalidDistribution", `${what} must be an integer from 0 to ${U32_MAX}`);
89
+ return value;
90
+ }
75
91
  /** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
76
92
  function tierAmount(base, multiplierBps) {
77
- return checkedU64(base * BigInt(multiplierBps) / BPS, "tier amount");
93
+ return checkedU64(base * BigInt(checkedU32(multiplierBps, "multiplierBps")) / BPS, "tier amount");
78
94
  }
79
95
  /**
80
96
  * `math::validate`. Checks the table alone, with no base.
@@ -89,7 +105,9 @@ function validateTiers(tiers) {
89
105
  if (tiers.length !== 8) throw new GaboxMathError("InvalidDistribution", `expected 8 tiers, got ${tiers.length}`);
90
106
  let count = 0n;
91
107
  let expected = 0n;
92
- for (const tier of tiers) {
108
+ for (const [index, tier] of tiers.entries()) {
109
+ checkedU32(tier.tickets, `tier ${index} tickets`);
110
+ checkedU32(tier.multiplierBps, `tier ${index} multiplierBps`);
93
111
  if (tier.tickets === 0) {
94
112
  if (tier.multiplierBps !== 0) throw new GaboxMathError("InvalidDistribution", "a tier with no tickets must have no multiplier");
95
113
  continue;
@@ -110,6 +128,7 @@ function validateTiers(tiers) {
110
128
  function validatePack(packTokens, tiers) {
111
129
  if (packTokens <= 0n) throw new GaboxMathError("ZeroAmount", "packTokens must be positive");
112
130
  checkedU64(packTokens, "pack tokens");
131
+ validateTiers(tiers);
113
132
  for (const [i, tier] of tiers.entries()) {
114
133
  if (tier.tickets === 0) continue;
115
134
  if (tierAmount(packTokens, tier.multiplierBps) === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
@@ -130,64 +149,18 @@ function validatePack(packTokens, tiers) {
130
149
  * The creator only signs a maximum SOL cost.
131
150
  */
132
151
  function seedTokens(packTokens, tiers) {
152
+ validateTiers(tiers);
133
153
  const largest = uncappedMaximum(packTokens, tiers);
134
154
  if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
135
155
  return largest - packTokens;
136
156
  }
137
157
  /**
138
- * Derive a fully valid four-outcome rarity ladder from a jackpot multiplier and a risk preset.
139
- *
140
- * Mythic is exactly `jackpotBps`. Rare and Epic interpolate between sub-1x launch values
141
- * and Mythic as the jackpot grows, so the four payouts remain strictly ordered even at 1x.
142
- * The profile assigns fixed slices of one pack's EV budget to the three higher rarities. Whatever
143
- * remains of the 95% target becomes Common. Integer rounding is always downward, so the result
144
- * cannot cross the program's 100% expected-value ceiling.
145
- *
146
- * The seed this table needs is `seedTokens(packTokens, tiers)`: `(jackpotBps - 10_000)` bps of
147
- * one pack.
148
- */
149
- function jackpotTiers(jackpotBps, profile) {
150
- const settings = RISK_PROFILES[profile];
151
- if (!settings) throw new GaboxMathError("InvalidDistribution", `unknown risk profile: ${profile}`);
152
- if (!Number.isInteger(jackpotBps) || jackpotBps < Number(10000n)) throw new GaboxMathError("JackpotBelowOnePack", "jackpotBps must be a whole number of at least 10_000 (1x)");
153
- if (BigInt(jackpotBps) > U32_MAX) throw new GaboxMathError("Arithmetic", "jackpotBps does not fit in u32");
154
- const maximum = BigInt(jackpotBps);
155
- const totalTickets = BigInt(TICKETS);
156
- const growth = maximum - BPS;
157
- const rarityMultipliers = [
158
- 9600n + growth / 8n,
159
- 9800n + growth / 3n,
160
- maximum
161
- ];
162
- const rarityTickets = rarityMultipliers.map((multiplier, index) => settings.rarityEvBps[index] * totalTickets / multiplier);
163
- if (rarityTickets.some((tickets) => tickets < 1n)) throw new GaboxMathError("UnfundedExpectation", `jackpot ladder is too large for the ${profile} profile's minimum one-ticket odds`);
164
- const commonTickets = totalTickets - rarityTickets.reduce((sum, tickets) => sum + tickets, 0n);
165
- if (commonTickets < 1n) throw new GaboxMathError("InvalidDistribution", "risk profile leaves no Common tickets");
166
- const rarityExpected = rarityTickets.reduce((sum, tickets, index) => sum + rarityMultipliers[index] * tickets, 0n);
167
- const commonMultiplier = (settings.targetEvBps * totalTickets - rarityExpected) / commonTickets;
168
- if (commonMultiplier < 1n || commonMultiplier >= rarityMultipliers[0]) throw new GaboxMathError("PackTooSmall", "risk profile cannot derive an ordered, non-zero Common multiplier");
169
- const tiers = Array.from({ length: 8 }, () => ({
170
- multiplierBps: 0,
171
- tickets: 0
172
- }));
173
- tiers[0] = {
174
- multiplierBps: Number(commonMultiplier),
175
- tickets: Number(commonTickets)
176
- };
177
- for (let index = 0; index < rarityMultipliers.length; index += 1) tiers[index + 1] = {
178
- multiplierBps: Number(rarityMultipliers[index]),
179
- tickets: Number(rarityTickets[index])
180
- };
181
- validateTiers(tiers);
182
- if (maxMultiplierBps(tiers) !== jackpotBps) throw new GaboxMathError("InvalidDistribution", "the ladder lost its jackpot tier");
183
- return tiers;
184
- }
185
- /**
186
158
  * `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
187
159
  *
188
160
  * At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
189
161
  */
190
162
  function uncappedMaximum(base, tiers) {
163
+ validateTiers(tiers);
191
164
  let largest = 0n;
192
165
  for (const tier of tiers) {
193
166
  if (tier.tickets === 0) continue;
@@ -208,6 +181,7 @@ function uncappedMaximum(base, tiers) {
208
181
  * error: the pool must not sell a ticket that can only win nothing.
209
182
  */
210
183
  function quote(base, tiers, inventory, reserved) {
184
+ validateTiers(tiers);
211
185
  if (inventory < reserved) throw new GaboxMathError("InsolventInventory", "the vault holds less than the pool has already reserved");
212
186
  const available = checkedU64(inventory - reserved + base, "available inventory");
213
187
  const prizes = Array.from({ length: 8 }, () => ({
@@ -256,6 +230,7 @@ function resolveReservation(reserved, maximum, award) {
256
230
  }
257
231
  /** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
258
232
  function maxMultiplierBps(tiers) {
233
+ validateTiers(tiers);
259
234
  let largest = 0;
260
235
  for (const tier of tiers) {
261
236
  if (tier.tickets === 0) continue;
@@ -270,6 +245,7 @@ function maxMultiplierBps(tiers) {
270
245
  * vault on average, which is what lets a pool survive a run of top-tier wins.
271
246
  */
272
247
  function averageMultiplierBps(tiers) {
248
+ validateTiers(tiers);
273
249
  let weighted = 0n;
274
250
  for (const tier of tiers) {
275
251
  if (tier.tickets === 0) continue;
@@ -1328,11 +1304,14 @@ function wsolPreparation$2(purchaser, lamports) {
1328
1304
  * straight into the vault. The Pump buy accounts ride along as `remainingAccounts`, in the same
1329
1305
  * order `buy_pack` uses.
1330
1306
  *
1331
- * The seed follows from the jackpot. A creator chooses a jackpot multiplier and a risk profile,
1332
- * and the SDK derives a valid tier table. The program then buys exactly
1333
- * `(jackpot - 1x) * PACK_TOKENS` tokens as the seed, so the first pack can pay the jackpot in
1334
- * full. The creator only signs a maximum SOL cost for that buy. The seed buy moves the curve, so
1335
- * a bigger jackpot means a slightly higher starting pack price.
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.
1336
1315
  *
1337
1316
  * # Two signers
1338
1317
  *
@@ -1347,10 +1326,14 @@ function wsolPreparation$2(purchaser, lamports) {
1347
1326
  * does not exist yet, so every other account is a derivation.
1348
1327
  */
1349
1328
  async function createMachine(client, input) {
1350
- const { creator, mintKeypair, name, symbol, uri, feeBps, riskProfile, jackpotBps, maxSeedLamports } = input;
1329
+ const { creator, mintKeypair, name, symbol, uri, feeBps, maxSeedLamports } = input;
1351
1330
  if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 100) throw new Error(`feeBps must be a whole number from 0 to 100`);
1352
- const tiers = jackpotTiers(jackpotBps, riskProfile);
1353
- if (seedTokens(PACK_TOKENS, tiers) > 0n && maxSeedLamports <= 0n) throw new Error("maxSeedLamports must be positive when the jackpot needs a seed");
1331
+ const tiers = cloneTiers(input.tiers ?? DEFAULT_TIERS);
1332
+ validateTiers(tiers);
1333
+ 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);
1354
1337
  const mint = mintKeypair.address;
1355
1338
  const create = getPumpCreateV2Instruction({
1356
1339
  mint: mintKeypair,
@@ -1383,21 +1366,50 @@ async function createMachine(client, input) {
1383
1366
  });
1384
1367
  }
1385
1368
  /**
1386
- * What the seed for this jackpot costs, fees included, and how many tokens it is.
1369
+ * What the seed for this table costs, fees included, and how many tokens it is.
1387
1370
  *
1388
1371
  * The coin does not exist yet, so the price is Pump's default new curve. Nothing else trades on
1389
1372
  * it before `initialize_pool` runs in the same transaction, so this is exact up to a change in
1390
1373
  * Pump's fee settings between the read and the send. Add a small margin for `maxSeedLamports`.
1374
+ *
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.
1391
1377
  */
1392
- async function seedCostEstimate(client, jackpotBps, riskProfile) {
1393
- const tiers = jackpotTiers(jackpotBps, riskProfile);
1394
- const seed = seedTokens(PACK_TOKENS, tiers);
1378
+ async function seedCostEstimate(client, tiers = DEFAULT_TIERS) {
1379
+ const copiedTiers = cloneTiers(tiers);
1380
+ validateTiers(copiedTiers);
1381
+ validatePack(PACK_TOKENS, copiedTiers);
1382
+ const seed = seedTokens(PACK_TOKENS, copiedTiers);
1383
+ await assertSeedFitsFreshCurve(client, seed);
1395
1384
  return {
1396
- tiers,
1385
+ tiers: copiedTiers,
1397
1386
  seedTokens: seed,
1398
1387
  lamports: seed === 0n ? 0n : await newCurveBuyCost(client, seed)
1399
1388
  };
1400
1389
  }
1390
+ /** A mutable encoded-table shape, owned by this call and safe to pass to Codama's builder. */
1391
+ function cloneTiers(tiers) {
1392
+ return tiers.map(({ multiplierBps, tickets }) => ({
1393
+ multiplierBps,
1394
+ tickets
1395
+ }));
1396
+ }
1397
+ /**
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.
1402
+ */
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
+ }
1401
1413
  /**
1402
1414
  * The Pump buy accounts for the seed, built without reading the bonding curve.
1403
1415
  *
@@ -1473,9 +1485,9 @@ async function expireDraw(client, input) {
1473
1485
  /**
1474
1486
  * What a client may do to a draw right now.
1475
1487
  *
1476
- * Reads the draw and the current slot, and reproduces the program's three retry conditions and its
1477
- * one expiry condition. Showing a disabled button with a countdown beats sending a transaction that
1478
- * fails with `RetryTooSoon`.
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`.
1479
1491
  */
1480
1492
  async function drawAvailability(client, draw) {
1481
1493
  const record = await fetchDraw(client, draw);
@@ -1758,6 +1770,6 @@ async function oracleAccounts() {
1758
1770
  };
1759
1771
  }
1760
1772
  //#endregion
1761
- 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, 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, jackpotTiers, 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 };
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 };
1762
1774
 
1763
1775
  //# sourceMappingURL=index.js.map