@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 +23 -1
- package/README.md +23 -21
- package/dist/index.d.ts +31 -35
- package/dist/index.js +109 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/gabox-sdk/SKILL.md +10 -9
- package/skills/gabox-sdk/references/api.md +7 -8
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
|
-
## [
|
|
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
|
|
39
|
-
tokens that back the
|
|
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
|
|
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
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
354
|
-
the purchase. It pays the **smallest prize** on the draw's table. It is not a refund.
|
|
355
|
-
|
|
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
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
/**
|
|
48
|
-
|
|
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
|
|
600
|
-
* `
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
681
|
-
*
|
|
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 =
|
|
39
|
+
const U32_MAX = 4294967295;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
1332
|
-
*
|
|
1333
|
-
* `
|
|
1334
|
-
* full. The creator only signs a maximum SOL cost for
|
|
1335
|
-
* a bigger jackpot means a slightly higher starting
|
|
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,
|
|
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 =
|
|
1353
|
-
|
|
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
|
|
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,
|
|
1393
|
-
const
|
|
1394
|
-
|
|
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
|
|
1477
|
-
*
|
|
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,
|
|
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
|