@gabox-labs/sdk 0.1.1 → 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/CHANGELOG.md +249 -1
- package/README.md +116 -458
- package/dist/generated/index.d.ts +334 -755
- package/dist/generated/index.js +2403 -338
- package/dist/generated/index.js.map +1 -1
- package/dist/index-BDfGvmgF.d.ts +934 -0
- package/dist/index.d.ts +211 -472
- package/dist/index.js +539 -1093
- package/dist/index.js.map +1 -1
- package/dist/raydium/index.d.ts +2 -0
- package/dist/raydium/index.js +2 -0
- package/dist/raydium-B-l9V3O-.js +2604 -0
- package/dist/raydium-B-l9V3O-.js.map +1 -0
- package/llms.txt +4 -2
- package/package.json +11 -16
- package/skills/gabox-sdk/SKILL.md +40 -141
- package/skills/gabox-sdk/references/api.md +102 -145
- package/dist/gaboxV2-CV2XltqC.js +0 -3347
- package/dist/gaboxV2-CV2XltqC.js.map +0 -1
- package/dist/index-BxvSkzCO.d.ts +0 -471
- package/dist/pump/index.d.ts +0 -2
- package/dist/pump/index.js +0 -2
- package/dist/pump-D0K_0uiC.js +0 -1531
- package/dist/pump-D0K_0uiC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,27 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
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. */
|
|
@@ -36,52 +78,69 @@ var GaboxMathError = class extends Error {
|
|
|
36
78
|
}
|
|
37
79
|
};
|
|
38
80
|
const U64_MAX = (1n << 64n) - 1n;
|
|
39
|
-
const U32_MAX =
|
|
81
|
+
const U32_MAX = 4294967295;
|
|
40
82
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
83
|
+
* The default prize table. The program does not enforce this table: it accepts any table that
|
|
84
|
+
* passes `validateTiers`. This is only the table the Gabox app creates its pools with, and the
|
|
85
|
+
* starting point for a client that has no reason to pick another one.
|
|
86
|
+
*
|
|
87
|
+
* Common 75% at 0.52x, Rare 20% at 1.2x, Epic 4% at 3x, Mythic 1% at 20x. Expected payout is
|
|
88
|
+
* 0.9499x of a pack, so the seed is 19 packs.
|
|
44
89
|
*/
|
|
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
|
-
|
|
90
|
+
const DEFAULT_TIERS = Object.freeze([
|
|
91
|
+
Object.freeze({
|
|
92
|
+
multiplierBps: 5201,
|
|
93
|
+
tickets: 49153
|
|
94
|
+
}),
|
|
95
|
+
Object.freeze({
|
|
96
|
+
multiplierBps: 12e3,
|
|
97
|
+
tickets: 13107
|
|
98
|
+
}),
|
|
99
|
+
Object.freeze({
|
|
100
|
+
multiplierBps: 3e4,
|
|
101
|
+
tickets: 2621
|
|
102
|
+
}),
|
|
103
|
+
Object.freeze({
|
|
104
|
+
multiplierBps: 2e5,
|
|
105
|
+
tickets: 655
|
|
106
|
+
}),
|
|
107
|
+
Object.freeze({
|
|
108
|
+
multiplierBps: 0,
|
|
109
|
+
tickets: 0
|
|
110
|
+
}),
|
|
111
|
+
Object.freeze({
|
|
112
|
+
multiplierBps: 0,
|
|
113
|
+
tickets: 0
|
|
114
|
+
}),
|
|
115
|
+
Object.freeze({
|
|
116
|
+
multiplierBps: 0,
|
|
117
|
+
tickets: 0
|
|
118
|
+
}),
|
|
119
|
+
Object.freeze({
|
|
120
|
+
multiplierBps: 0,
|
|
121
|
+
tickets: 0
|
|
122
|
+
})
|
|
123
|
+
]);
|
|
71
124
|
function checkedU64(value, what) {
|
|
72
125
|
if (value < 0n || value > U64_MAX) throw new GaboxMathError("Arithmetic", `${what} does not fit in u64`);
|
|
73
126
|
return value;
|
|
74
127
|
}
|
|
128
|
+
/** Tier fields encode as program `u32`s. Reject JavaScript-only values before `BigInt` or codecs. */
|
|
129
|
+
function checkedU32(value, what) {
|
|
130
|
+
if (!Number.isInteger(value) || value < 0 || value > U32_MAX) throw new GaboxMathError("InvalidDistribution", `${what} must be an integer from 0 to ${U32_MAX}`);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
75
133
|
/** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
|
|
76
134
|
function tierAmount(base, multiplierBps) {
|
|
77
|
-
return checkedU64(base * BigInt(multiplierBps) / BPS, "tier amount");
|
|
135
|
+
return checkedU64(base * BigInt(checkedU32(multiplierBps, "multiplierBps")) / BPS, "tier amount");
|
|
78
136
|
}
|
|
79
137
|
/**
|
|
80
138
|
* `math::validate`. Checks the table alone, with no base.
|
|
81
139
|
*
|
|
82
|
-
*
|
|
140
|
+
* Four rules, and each one closes a way to sell a bad ticket:
|
|
83
141
|
* - ticket counts sum to exactly 65,536, so the 16-bit draw is uniform;
|
|
84
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);
|
|
85
144
|
* - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
|
|
86
145
|
* tokens than a pack buys. This bounds tokens, not cash value.
|
|
87
146
|
*/
|
|
@@ -89,12 +148,15 @@ function validateTiers(tiers) {
|
|
|
89
148
|
if (tiers.length !== 8) throw new GaboxMathError("InvalidDistribution", `expected 8 tiers, got ${tiers.length}`);
|
|
90
149
|
let count = 0n;
|
|
91
150
|
let expected = 0n;
|
|
92
|
-
for (const tier of tiers) {
|
|
151
|
+
for (const [index, tier] of tiers.entries()) {
|
|
152
|
+
checkedU32(tier.tickets, `tier ${index} tickets`);
|
|
153
|
+
checkedU32(tier.multiplierBps, `tier ${index} multiplierBps`);
|
|
93
154
|
if (tier.tickets === 0) {
|
|
94
155
|
if (tier.multiplierBps !== 0) throw new GaboxMathError("InvalidDistribution", "a tier with no tickets must have no multiplier");
|
|
95
156
|
continue;
|
|
96
157
|
}
|
|
97
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)`);
|
|
98
160
|
count += BigInt(tier.tickets);
|
|
99
161
|
expected += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
100
162
|
}
|
|
@@ -110,77 +172,37 @@ function validateTiers(tiers) {
|
|
|
110
172
|
function validatePack(packTokens, tiers) {
|
|
111
173
|
if (packTokens <= 0n) throw new GaboxMathError("ZeroAmount", "packTokens must be positive");
|
|
112
174
|
checkedU64(packTokens, "pack tokens");
|
|
175
|
+
validateTiers(tiers);
|
|
113
176
|
for (const [i, tier] of tiers.entries()) {
|
|
114
177
|
if (tier.tickets === 0) continue;
|
|
115
178
|
if (tierAmount(packTokens, tier.multiplierBps) === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
116
179
|
}
|
|
117
180
|
}
|
|
118
181
|
/**
|
|
119
|
-
* `math::seed_tokens`. The seed a table needs, in tokens.
|
|
182
|
+
* `math::seed_tokens`. The mandatory seed a table needs, in tokens.
|
|
120
183
|
*
|
|
121
|
-
* The
|
|
122
|
-
*
|
|
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:
|
|
123
188
|
*
|
|
124
|
-
* seedTokens = uncappedMaximum(packTokens, tiers) - packTokens
|
|
189
|
+
* seedTokens = min(uncappedMaximum(packTokens, tiers), tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS)) - packTokens
|
|
125
190
|
*
|
|
126
|
-
* A
|
|
127
|
-
*
|
|
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.
|
|
128
196
|
*
|
|
129
|
-
*
|
|
197
|
+
* Pool initialization computes this number itself and buys exactly that many tokens on the curve.
|
|
130
198
|
* The creator only signs a maximum SOL cost.
|
|
131
199
|
*/
|
|
132
200
|
function seedTokens(packTokens, tiers) {
|
|
201
|
+
validateTiers(tiers);
|
|
133
202
|
const largest = uncappedMaximum(packTokens, tiers);
|
|
134
203
|
if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
/**
|
|
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;
|
|
204
|
+
const minimum = tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS);
|
|
205
|
+
return (largest < minimum ? largest : minimum) - packTokens;
|
|
184
206
|
}
|
|
185
207
|
/**
|
|
186
208
|
* `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
|
|
@@ -188,6 +210,7 @@ function jackpotTiers(jackpotBps, profile) {
|
|
|
188
210
|
* At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
|
|
189
211
|
*/
|
|
190
212
|
function uncappedMaximum(base, tiers) {
|
|
213
|
+
validateTiers(tiers);
|
|
191
214
|
let largest = 0n;
|
|
192
215
|
for (const tier of tiers) {
|
|
193
216
|
if (tier.tickets === 0) continue;
|
|
@@ -208,6 +231,7 @@ function uncappedMaximum(base, tiers) {
|
|
|
208
231
|
* error: the pool must not sell a ticket that can only win nothing.
|
|
209
232
|
*/
|
|
210
233
|
function quote(base, tiers, inventory, reserved) {
|
|
234
|
+
validateTiers(tiers);
|
|
211
235
|
if (inventory < reserved) throw new GaboxMathError("InsolventInventory", "the vault holds less than the pool has already reserved");
|
|
212
236
|
const available = checkedU64(inventory - reserved + base, "available inventory");
|
|
213
237
|
const prizes = Array.from({ length: 8 }, () => ({
|
|
@@ -256,6 +280,7 @@ function resolveReservation(reserved, maximum, award) {
|
|
|
256
280
|
}
|
|
257
281
|
/** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
|
|
258
282
|
function maxMultiplierBps(tiers) {
|
|
283
|
+
validateTiers(tiers);
|
|
259
284
|
let largest = 0;
|
|
260
285
|
for (const tier of tiers) {
|
|
261
286
|
if (tier.tickets === 0) continue;
|
|
@@ -270,6 +295,7 @@ function maxMultiplierBps(tiers) {
|
|
|
270
295
|
* vault on average, which is what lets a pool survive a run of top-tier wins.
|
|
271
296
|
*/
|
|
272
297
|
function averageMultiplierBps(tiers) {
|
|
298
|
+
validateTiers(tiers);
|
|
273
299
|
let weighted = 0n;
|
|
274
300
|
for (const tier of tiers) {
|
|
275
301
|
if (tier.tickets === 0) continue;
|
|
@@ -280,589 +306,284 @@ function averageMultiplierBps(tiers) {
|
|
|
280
306
|
//#endregion
|
|
281
307
|
//#region src/pdas.ts
|
|
282
308
|
/**
|
|
283
|
-
*
|
|
309
|
+
* Gabox's own program-derived addresses.
|
|
284
310
|
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
/** `['referral', referee]`. A wallet's permanent referral binding. */
|
|
291
|
-
async function referralLinkAddress(referee) {
|
|
292
|
-
return (await findReferralLinkPda({ purchaser: referee }))[0];
|
|
293
|
-
}
|
|
294
|
-
/** `['referral', pool, referrer]`. A referrer's accrued rewards for one pool. */
|
|
295
|
-
async function referralAddress(pool, referrer) {
|
|
296
|
-
return (await findReferralPda({
|
|
297
|
-
pool,
|
|
298
|
-
referralLink: referrer
|
|
299
|
-
}))[0];
|
|
300
|
-
}
|
|
301
|
-
/** `["pool", mint]`. One pool per coin, and the mint alone is the seed. */
|
|
302
|
-
async function poolAddress(mint) {
|
|
303
|
-
return (await findPoolPda({ mint }))[0];
|
|
304
|
-
}
|
|
305
|
-
/** `["draw", pool, seq_u64_le]`. `seq` is `pool.nextSeq` at the moment of the purchase. */
|
|
306
|
-
async function drawAddress(pool, seq) {
|
|
307
|
-
return (await findDrawPda({
|
|
308
|
-
pool,
|
|
309
|
-
seq
|
|
310
|
-
}))[0];
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* `["identity"]` under gabox. This is the PDA gabox signs the VRF request with — the `identity`
|
|
314
|
-
* account of the `Oracle` group on `buy_pack` and `retry_draw`.
|
|
315
|
-
*/
|
|
316
|
-
async function vrfIdentityAddress() {
|
|
317
|
-
return (await findIdentityPda())[0];
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* `["identity", gabox_program_id]` under the **VRF program**. A different address from
|
|
321
|
-
* `vrfIdentityAddress`, and it belongs to the other side: MagicBlock signs the `deliver_draw`
|
|
322
|
-
* callback with it. A client never puts it in an instruction. It is here so a client can recognise
|
|
323
|
-
* 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.
|
|
324
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. */
|
|
325
329
|
async function scopedVrfIdentityAddress() {
|
|
326
330
|
return await getProgramDerivedAddress({
|
|
327
331
|
programAddress: VRF_PROGRAM_ADDRESS,
|
|
328
332
|
seeds: [getBytesEncoder().encode(IDENTITY_SEED), getAddressEncoder().encode(GABOX_PROGRAM_ID)]
|
|
329
333
|
});
|
|
330
334
|
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
async function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
|
|
336
|
-
const encoder = getAddressEncoder();
|
|
337
|
-
const [address] = await getProgramDerivedAddress({
|
|
338
|
-
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
|
|
339
|
-
seeds: [
|
|
340
|
-
encoder.encode(owner),
|
|
341
|
-
encoder.encode(tokenProgram),
|
|
342
|
-
encoder.encode(mint)
|
|
343
|
-
]
|
|
344
|
-
});
|
|
345
|
-
return address;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* The protocol fee collector's WSOL ATA, under classic SPL Token. A sale on PumpSwap pays its
|
|
349
|
-
* protocol fee here. The account must exist before the first PumpSwap sale; see `DEPLOYMENT.md`.
|
|
350
|
-
*/
|
|
351
|
-
async function feeCollectorWsolAddress() {
|
|
352
|
-
return await associatedTokenAddress(PROTOCOL_FEE_COLLECTOR, WSOL_MINT, TOKEN_PROGRAM_ADDRESS);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* The pool's prize inventory: the pool PDA's ATA for the mint's own token program.
|
|
356
|
-
*
|
|
357
|
-
* `tokenProgram` defaults to Token-2022 because Pump `create_v2` mints there, and every machine is
|
|
358
|
-
* built on a coin Pump created. Pass the mint's real owner if you have it.
|
|
359
|
-
*/
|
|
360
|
-
async function vaultAddress(mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
|
|
361
|
-
return await associatedTokenAddress(await poolAddress(mint), mint, tokenProgram);
|
|
362
|
-
}
|
|
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);
|
|
363
339
|
//#endregion
|
|
364
340
|
//#region src/accounts.ts
|
|
365
|
-
/**
|
|
366
|
-
* Reading gabox state: pools, draws, and the vault balance that turns a pool into an offer.
|
|
367
|
-
*
|
|
368
|
-
* The decoders are the generated ones. What this file adds is the queries — the discriminator and
|
|
369
|
-
* `memcmp` filters that let a client list every machine, or every draw a wallet is waiting on,
|
|
370
|
-
* without an indexer.
|
|
371
|
-
*
|
|
372
|
-
* # The offsets are computed, not counted
|
|
373
|
-
*
|
|
374
|
-
* A `memcmp` filter is a byte offset into an account. Counting field widths by hand is how a filter
|
|
375
|
-
* silently matches nothing. Each offset below is built from the widths of the fields before it, in
|
|
376
|
-
* the same order `state.rs` declares them, so the arithmetic is visible and a field inserted in the
|
|
377
|
-
* middle changes it.
|
|
378
|
-
*/
|
|
341
|
+
/** Account readers and bounded discovery scans. Old pool layouts are intentionally unsupported. */
|
|
379
342
|
const DISCRIMINATOR = 8;
|
|
380
|
-
/** `Draw.pool` sits straight after the discriminator. */
|
|
381
343
|
const DRAW_POOL_OFFSET = DISCRIMINATOR;
|
|
382
|
-
/** `Draw.purchaser` sits after the discriminator and `pool`. */
|
|
383
344
|
const DRAW_PURCHASER_OFFSET = 40;
|
|
384
|
-
/** `Pool.creator` sits straight after the discriminator. */
|
|
385
345
|
const POOL_CREATOR_OFFSET = DISCRIMINATOR;
|
|
386
|
-
/** `Pool.mint` sits after the discriminator and `creator`. */
|
|
387
346
|
const POOL_MINT_OFFSET = 40;
|
|
388
|
-
/** `ReferralLink.referrer` sits after the discriminator and `referee`. */
|
|
389
|
-
const REFERRAL_LINK_REFERRER_OFFSET = 40;
|
|
390
347
|
const base58 = getBase58Decoder();
|
|
391
|
-
const base64
|
|
392
|
-
/** A `memcmp` filter wants base58, and both discriminators and addresses arrive as other things. */
|
|
348
|
+
const base64 = getBase64Encoder();
|
|
393
349
|
const asBase58 = (bytes) => base58.decode(bytes);
|
|
394
350
|
const memcmp = (offset, bytes) => ({ memcmp: {
|
|
395
351
|
offset: BigInt(offset),
|
|
396
352
|
bytes,
|
|
397
353
|
encoding: "base58"
|
|
398
354
|
} });
|
|
399
|
-
async function scan(rpc, filters, decode) {
|
|
400
|
-
return (await rpc.getProgramAccounts(GABOX_PROGRAM_ID, {
|
|
401
|
-
encoding: "base64",
|
|
402
|
-
commitment: "confirmed",
|
|
403
|
-
filters
|
|
404
|
-
}).send()).map(({ pubkey, account }) => decode({
|
|
405
|
-
address: pubkey,
|
|
406
|
-
data: new Uint8Array(base64$1.encode(account.data[0]))
|
|
407
|
-
}));
|
|
408
|
-
}
|
|
409
|
-
/** Wrap raw bytes in the shape the generated decoders expect. */
|
|
410
355
|
const encoded = (address, data) => ({
|
|
411
356
|
address,
|
|
412
357
|
data,
|
|
358
|
+
exists: true,
|
|
413
359
|
executable: false,
|
|
414
360
|
lamports: 0n,
|
|
415
361
|
programAddress: GABOX_PROGRAM_ID,
|
|
416
362
|
space: BigInt(data.length)
|
|
417
363
|
});
|
|
418
|
-
/** The pool for a coin, or `null` when the coin has no machine. */
|
|
419
364
|
async function fetchPoolByMint(client, mint) {
|
|
420
365
|
return await fetchPoolAt(client, await poolAddress(mint));
|
|
421
366
|
}
|
|
422
|
-
/**
|
|
423
|
-
* A pool by its own address, or `null`.
|
|
424
|
-
*
|
|
425
|
-
* `fetchPoolByMint` is the usual way in, because a coin's mint is the natural key. This one is for
|
|
426
|
-
* the other direction: a `Draw` names its pool and not its mint, so a client holding a draw reads
|
|
427
|
-
* the pool to learn which coin it belongs to.
|
|
428
|
-
*/
|
|
429
367
|
async function fetchPoolAt(client, address) {
|
|
430
368
|
return decodeCurrentPool(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
431
369
|
}
|
|
432
|
-
/** `null` for a missing pool; an error for one on the older devnet layout. */
|
|
433
370
|
function decodeCurrentPool(account) {
|
|
434
371
|
if (!account.exists) return null;
|
|
435
|
-
if (account.data.length !== getPoolSize()) throw new Error(`
|
|
372
|
+
if (account.data.length !== getPoolSize()) throw new Error(`pool ${account.address} is not a Gabox pool and is intentionally unsupported`);
|
|
436
373
|
return decodePool(account).data;
|
|
437
374
|
}
|
|
438
|
-
|
|
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
|
+
}
|
|
439
380
|
async function fetchDraw(client, address) {
|
|
440
|
-
|
|
441
|
-
return account.exists ? account.data : null;
|
|
381
|
+
return decodeCurrentDraw(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
442
382
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
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;
|
|
448
387
|
}
|
|
449
388
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* There is no on-chain registry — the design says so on purpose — so discovery is this scan plus
|
|
454
|
-
* the `PoolCreated` event. Public RPCs limit `getProgramAccounts`, so an app that lists machines
|
|
455
|
-
* 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.
|
|
456
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
|
+
}
|
|
457
402
|
async function listPools(client) {
|
|
458
|
-
return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], (
|
|
403
|
+
return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], (address, data) => ({
|
|
459
404
|
address,
|
|
460
405
|
data: decodePool(encoded(address, data)).data
|
|
461
406
|
}));
|
|
462
407
|
}
|
|
463
|
-
/** Every draw of one pool, pending or ready. Closed draws are gone and never appear. */
|
|
464
|
-
async function listDrawsByPool(client, pool) {
|
|
465
|
-
return await listDraws(client, { pool });
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Every open draw of one wallet, across all pools. This is the "what am I owed" query.
|
|
469
|
-
*
|
|
470
|
-
* An address is already base58, so it goes into the filter unchanged.
|
|
471
|
-
*/
|
|
472
|
-
async function listDrawsByPurchaser(client, purchaser) {
|
|
473
|
-
return await listDraws(client, { purchaser });
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Open draws, narrowed by pool, by purchaser, or by both.
|
|
477
|
-
*
|
|
478
|
-
* Both filters in one scan, because a UI asks for exactly that: "my pulls on this machine". Two
|
|
479
|
-
* separate scans and an intersection in the client would move twice the bytes and could disagree
|
|
480
|
-
* with itself, since the two reads happen at different slots. With neither filter this lists every
|
|
481
|
-
* open draw of every machine.
|
|
482
|
-
*/
|
|
483
408
|
async function listDraws(client, query = {}) {
|
|
484
|
-
const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR))];
|
|
409
|
+
const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR)), { dataSize: BigInt(getDrawSize()) }];
|
|
485
410
|
if (query.pool) filters.push(memcmp(8, query.pool));
|
|
486
411
|
if (query.purchaser) filters.push(memcmp(40, query.purchaser));
|
|
487
|
-
return await scan(client.rpc, filters, (
|
|
412
|
+
return await scan(client.rpc, filters, (address, data) => ({
|
|
488
413
|
address,
|
|
489
414
|
data: decodeDraw(encoded(address, data)).data
|
|
490
415
|
}));
|
|
491
416
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
* The list is small in practice, and a dashboard reads it once per load, not per poll.
|
|
497
|
-
*/
|
|
498
|
-
async function listReferralLinksByReferrer(client, referrer) {
|
|
499
|
-
return await scan(client.rpc, [
|
|
500
|
-
memcmp(0, asBase58(REFERRAL_LINK_DISCRIMINATOR)),
|
|
501
|
-
{ dataSize: BigInt(getReferralLinkSize()) },
|
|
502
|
-
memcmp(40, referrer)
|
|
503
|
-
], ({ address, data }) => ({
|
|
504
|
-
address,
|
|
505
|
-
data: decodeReferralLink(encoded(address, data)).data
|
|
506
|
-
}));
|
|
507
|
-
}
|
|
508
|
-
/** The vault's token balance. `0` when the vault does not exist yet. */
|
|
509
|
-
async function fetchVaultBalance(client, mint, tokenProgram) {
|
|
510
|
-
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);
|
|
511
421
|
const { value } = await client.rpc.getAccountInfo(vault, {
|
|
512
422
|
encoding: "base64",
|
|
513
423
|
commitment: "confirmed"
|
|
514
424
|
}).send();
|
|
515
|
-
|
|
516
|
-
return tokenAccountAmount(new Uint8Array(base64$1.encode(value.data[0])));
|
|
425
|
+
return value ? tokenAccountAmount(new Uint8Array(base64.encode(value.data[0]))) : 0n;
|
|
517
426
|
}
|
|
518
|
-
/**
|
|
519
|
-
* A pool with its live inventory. This is the pair every price display needs: the tiers are
|
|
520
|
-
* immutable, but what they actually pay depends on what is free in the vault right now.
|
|
521
|
-
*
|
|
522
|
-
* # Why one request reads both accounts
|
|
523
|
-
*
|
|
524
|
-
* The oracle callback lowers the vault and `pool.reserved` in one transaction, a few seconds after
|
|
525
|
-
* a buy. Two separate reads can land on either side of it: the old `reserved` with the new, smaller
|
|
526
|
-
* vault. That pair looks insolvent, and `quote` rightly refuses it. One `getMultipleAccounts` call
|
|
527
|
-
* returns both accounts from the same slot, so the pair is always one the chain actually held.
|
|
528
|
-
*/
|
|
529
427
|
async function fetchPoolInventory(client, mint) {
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (!pair) return null;
|
|
544
|
-
const { pool, inventory } = pair;
|
|
545
|
-
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;
|
|
546
441
|
return {
|
|
442
|
+
poolAddress: poolPda,
|
|
547
443
|
pool,
|
|
548
|
-
poolAddress: address,
|
|
549
|
-
vault: pool.vault,
|
|
550
444
|
inventory,
|
|
551
|
-
reserved,
|
|
552
|
-
free: inventory
|
|
445
|
+
reserved: pool.reserved,
|
|
446
|
+
free: inventory > pool.reserved ? inventory - pool.reserved : 0n
|
|
553
447
|
};
|
|
554
448
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
tickets: t.tickets
|
|
449
|
+
const tiersOf = (pool) => pool.tiers.map(({ multiplierBps, tickets }) => ({
|
|
450
|
+
multiplierBps,
|
|
451
|
+
tickets
|
|
559
452
|
}));
|
|
560
|
-
|
|
561
|
-
* The offer a pack of `base` tokens would freeze against this inventory. Pure — the same
|
|
562
|
-
* computation `buy_pack` performs, and the reason a client can show real amounts before paying.
|
|
563
|
-
*/
|
|
564
|
-
const offerFor = (inventory, base) => quote(base, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
|
|
565
|
-
//#endregion
|
|
566
|
-
//#region src/compute.ts
|
|
567
|
-
/**
|
|
568
|
-
* The two `ComputeBudget` instructions the transaction builders prepend.
|
|
569
|
-
*
|
|
570
|
-
* # Why every gabox transaction needs one
|
|
571
|
-
*
|
|
572
|
-
* The default budget is 200,000 compute units for the whole transaction. `buy_pack` alone does a
|
|
573
|
-
* CPI into Pump (which does its own CPIs into two token programs and its fee program), a
|
|
574
|
-
* `transfer_checked`, an account init, a system transfer, and a CPI into MagicBlock's VRF program
|
|
575
|
-
* that allocates a request account. `createMachine` adds Pump's `create_v2` to that, which mints a
|
|
576
|
-
* Token-2022 coin with metadata. Neither fits in the default, and nothing on chain can raise its
|
|
577
|
-
* own budget.
|
|
578
|
-
*
|
|
579
|
-
* # The numbers are headroom, not measurements
|
|
580
|
-
*
|
|
581
|
-
* They have not been measured on chain. They sit comfortably above what these instruction mixes
|
|
582
|
-
* plausibly cost and comfortably below the 1,400,000 ceiling. **Measure them on devnet before they
|
|
583
|
-
* matter**, because the prioritisation fee is `price x requested limit`: a request that is three
|
|
584
|
-
* times too large costs three times too much on every pack. Until then, treat them as a starting
|
|
585
|
-
* point a caller may override.
|
|
586
|
-
*/
|
|
587
|
-
const COMPUTE_BUDGET_PROGRAM_ADDRESS = "ComputeBudget111111111111111111111111111111";
|
|
588
|
-
/** The runtime's per-transaction ceiling. A larger request is rejected outright. */
|
|
589
|
-
const MAX_COMPUTE_UNIT_LIMIT = 14e5;
|
|
590
|
-
/** What an instruction gets when no `SetComputeUnitLimit` is present. */
|
|
591
|
-
const DEFAULT_COMPUTE_UNIT_LIMIT = 2e5;
|
|
592
|
-
/**
|
|
593
|
-
* UNVERIFIED: none of the three figures below has been measured on chain. They are headroom, chosen
|
|
594
|
-
* above what these instruction mixes plausibly cost. Measure them on devnet before they matter: the
|
|
595
|
-
* prioritisation fee is `price x requested limit`, so a request three times too large costs three
|
|
596
|
-
* times too much on every pack.
|
|
597
|
-
*/
|
|
598
|
-
/** Pump `create_v2` plus `initialize_pool` plus a seed buy, in one transaction. */
|
|
599
|
-
const CREATE_MACHINE_COMPUTE_UNITS = 6e5;
|
|
600
|
-
/** A venue buy, an escrow transfer, a draw init and a VRF request. */
|
|
601
|
-
const BUY_PACK_COMPUTE_UNITS = 5e5;
|
|
602
|
-
/** A vault transfer, or a vault transfer plus a venue sale. */
|
|
603
|
-
const REDEEM_COMPUTE_UNITS = 3e5;
|
|
604
|
-
/** `bind_referrer` riding ahead of a pack: one small account init. */
|
|
605
|
-
const BIND_REFERRER_COMPUTE_UNITS = 5e4;
|
|
606
|
-
/** `ComputeBudgetInstruction`'s discriminants. Positional and append-only upstream. */
|
|
607
|
-
const SET_COMPUTE_UNIT_LIMIT = 2;
|
|
608
|
-
const SET_COMPUTE_UNIT_PRICE = 3;
|
|
609
|
-
/** `[u8 discriminant, u32 units]` — five bytes. */
|
|
610
|
-
const LIMIT_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["units", getU32Encoder()]]);
|
|
611
|
-
/** `[u8 discriminant, u64 microLamports]` — nine bytes. */
|
|
612
|
-
const PRICE_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["microLamports", getU64Encoder()]]);
|
|
613
|
-
function getSetComputeUnitLimitInstruction(units) {
|
|
614
|
-
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}`);
|
|
615
|
-
return {
|
|
616
|
-
programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
|
|
617
|
-
accounts: [],
|
|
618
|
-
data: LIMIT_ENCODER.encode({
|
|
619
|
-
discriminant: SET_COMPUTE_UNIT_LIMIT,
|
|
620
|
-
units
|
|
621
|
-
})
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* `SetComputeUnitPrice(microLamports)` — the priority fee, per compute unit.
|
|
626
|
-
*
|
|
627
|
-
* No default, deliberately. The fee that lands a transaction is a property of the network at the
|
|
628
|
-
* moment you send it. A hardcoded price is either money burnt on an idle chain or a transaction
|
|
629
|
-
* that quietly stops landing under load. Sample `getRecentPrioritizationFees`, or take it from
|
|
630
|
-
* config.
|
|
631
|
-
*/
|
|
632
|
-
function getSetComputeUnitPriceInstruction(microLamports) {
|
|
633
|
-
const price = BigInt(microLamports);
|
|
634
|
-
if (price < 0n) throw new Error(`compute unit price must not be negative, got ${price}`);
|
|
635
|
-
return {
|
|
636
|
-
programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
|
|
637
|
-
accounts: [],
|
|
638
|
-
data: PRICE_ENCODER.encode({
|
|
639
|
-
discriminant: SET_COMPUTE_UNIT_PRICE,
|
|
640
|
-
microLamports: price
|
|
641
|
-
})
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
/** The compute budget prefix a builder prepends: a limit, and a price only when one is asked for. */
|
|
645
|
-
function computeBudgetInstructions(units, microLamports) {
|
|
646
|
-
const instructions = [getSetComputeUnitLimitInstruction(units)];
|
|
647
|
-
if (microLamports !== void 0) instructions.push(getSetComputeUnitPriceInstruction(microLamports));
|
|
648
|
-
return instructions;
|
|
649
|
-
}
|
|
453
|
+
const quotePool = (inventory) => quote(inventory.pool.packTokens, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
|
|
650
454
|
//#endregion
|
|
651
455
|
//#region src/events.ts
|
|
652
|
-
/**
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
* # Where the events are
|
|
656
|
-
*
|
|
657
|
-
* Anchor's `emit!` writes the event through `sol_log_data`, which the runtime renders as a log line
|
|
658
|
-
* `Program data: <base64>`. So decoding an event is: find those lines, base64-decode each one,
|
|
659
|
-
* match the first eight bytes against a discriminator, and hand the rest to the generated decoder.
|
|
660
|
-
*
|
|
661
|
-
* The design leans on this. There is no on-chain market registry, so `PoolCreated` is how a new
|
|
662
|
-
* machine is discovered, and `DrawResolved` stays readable after the `Draw` account is closed and
|
|
663
|
-
* its rent returned. The event is the audit trail; the account is only the pending state.
|
|
664
|
-
*
|
|
665
|
-
* # Waiting
|
|
666
|
-
*
|
|
667
|
-
* `watchDraw` subscribes to the draw account. A normal callback, retry callback, or expiry
|
|
668
|
-
* delivers the prize and closes that account; the watcher then reconstructs the result from the
|
|
669
|
-
* transaction's `PackBought` and `DrawResolved` events. It also accepts a legacy open `Ready`
|
|
670
|
-
* account from a callback requested before automatic delivery.
|
|
671
|
-
*/
|
|
672
|
-
const base64 = getBase64Encoder();
|
|
673
|
-
const startsWith = (data, discriminator) => {
|
|
674
|
-
if (data.length < discriminator.length) return false;
|
|
675
|
-
for (let i = 0; i < discriminator.length; i++) if (data[i] !== discriminator[i]) return false;
|
|
676
|
-
return true;
|
|
677
|
-
};
|
|
678
|
-
/** The prefix Anchor's `emit!` produces. Anything else in the log is not an event. */
|
|
679
|
-
const PROGRAM_DATA = "Program data: ";
|
|
680
|
-
/** 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);
|
|
681
459
|
function decodeEvent(data) {
|
|
682
|
-
if (
|
|
460
|
+
if (starts(data, POOL_CREATED_EVENT_DISCRIMINATOR)) return {
|
|
683
461
|
name: "PoolCreated",
|
|
684
462
|
data: getPoolCreatedEventDecoder().decode(data)
|
|
685
463
|
};
|
|
686
|
-
if (
|
|
464
|
+
if (starts(data, PRIZES_FUNDED_EVENT_DISCRIMINATOR)) return {
|
|
687
465
|
name: "PrizesFunded",
|
|
688
466
|
data: getPrizesFundedEventDecoder().decode(data)
|
|
689
467
|
};
|
|
690
|
-
if (
|
|
468
|
+
if (starts(data, PACK_BOUGHT_EVENT_DISCRIMINATOR)) return {
|
|
691
469
|
name: "PackBought",
|
|
692
470
|
data: getPackBoughtEventDecoder().decode(data)
|
|
693
471
|
};
|
|
694
|
-
if (
|
|
472
|
+
if (starts(data, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR)) return {
|
|
695
473
|
name: "RandomnessRetried",
|
|
696
474
|
data: getRandomnessRetriedEventDecoder().decode(data)
|
|
697
475
|
};
|
|
698
|
-
if (
|
|
476
|
+
if (starts(data, DRAW_RESOLVED_EVENT_DISCRIMINATOR)) return {
|
|
699
477
|
name: "DrawResolved",
|
|
700
478
|
data: getDrawResolvedEventDecoder().decode(data)
|
|
701
479
|
};
|
|
702
|
-
if (
|
|
703
|
-
name: "PrizeRedeemed",
|
|
704
|
-
data: getPrizeRedeemedEventDecoder().decode(data)
|
|
705
|
-
};
|
|
706
|
-
if (startsWith(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
|
|
480
|
+
if (starts(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
|
|
707
481
|
name: "TokensSold",
|
|
708
482
|
data: getTokensSoldEventDecoder().decode(data)
|
|
709
483
|
};
|
|
710
484
|
return null;
|
|
711
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: .*$`);
|
|
712
490
|
/**
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* rather than reported. Another program in the same transaction emits its own events, and they are
|
|
717
|
-
* 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.
|
|
718
494
|
*/
|
|
719
495
|
function decodeEvents(logs) {
|
|
720
|
-
const
|
|
496
|
+
const committed = [];
|
|
497
|
+
const stack = [];
|
|
498
|
+
const discardOpenFrames = () => {
|
|
499
|
+
stack.length = 0;
|
|
500
|
+
};
|
|
501
|
+
let malformed = false;
|
|
502
|
+
let transactionFailed = false;
|
|
721
503
|
for (const line of logs) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
+
});
|
|
728
515
|
continue;
|
|
729
516
|
}
|
|
730
|
-
const
|
|
731
|
-
|
|
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 {}
|
|
732
542
|
}
|
|
733
|
-
return
|
|
543
|
+
return malformed || transactionFailed || stack.length !== 0 ? [] : committed;
|
|
734
544
|
}
|
|
735
|
-
/** Fetch a transaction and decode the gabox events it emitted. */
|
|
736
545
|
async function fetchEvents(client, signature) {
|
|
737
546
|
return await readEvents(client.rpc, signature);
|
|
738
547
|
}
|
|
739
|
-
/** The RPC half of `fetchEvents`, for the internal readers that already hold one. */
|
|
740
548
|
async function readEvents(rpc, signature) {
|
|
741
|
-
|
|
549
|
+
const tx = await rpc.getTransaction(signature, {
|
|
742
550
|
commitment: "confirmed",
|
|
743
551
|
encoding: "json",
|
|
744
552
|
maxSupportedTransactionVersion: 0
|
|
745
|
-
}).send()
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const current = await readDraw(rpc, address);
|
|
768
|
-
if (current) {
|
|
769
|
-
options.onChange?.(current);
|
|
770
|
-
if (current.status === 1) return current;
|
|
771
|
-
} else {
|
|
772
|
-
const delivered = await readDeliveredDraw(rpc, address);
|
|
773
|
-
if (delivered) return delivered;
|
|
774
|
-
}
|
|
775
|
-
for await (const notification of notifications) {
|
|
776
|
-
const account = notification.value;
|
|
777
|
-
if (!account || account.data[0] === "") {
|
|
778
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
779
|
-
const delivered = await readDeliveredDraw(rpc, address);
|
|
780
|
-
if (delivered) return delivered;
|
|
781
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
782
|
-
}
|
|
783
|
-
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
|
+
};
|
|
784
575
|
}
|
|
785
|
-
const draw = decodeDrawBytes(address, new Uint8Array(base64.encode(account.data[0])));
|
|
786
|
-
options.onChange?.(draw);
|
|
787
|
-
if (draw.status === 1) return draw;
|
|
788
576
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
controller.abort();
|
|
577
|
+
if (rows.length < 100) return null;
|
|
578
|
+
before = rows.at(-1)?.signature;
|
|
579
|
+
if (!before) return null;
|
|
793
580
|
}
|
|
794
|
-
|
|
795
|
-
function decodeDrawBytes(address, data) {
|
|
796
|
-
return decodeDraw({
|
|
797
|
-
address,
|
|
798
|
-
data,
|
|
799
|
-
executable: false,
|
|
800
|
-
lamports: 0n,
|
|
801
|
-
programAddress: GABOX_PROGRAM_ID,
|
|
802
|
-
space: BigInt(data.length)
|
|
803
|
-
}).data;
|
|
804
|
-
}
|
|
805
|
-
async function readDraw(rpc, address) {
|
|
806
|
-
const { value } = await rpc.getAccountInfo(address, {
|
|
807
|
-
encoding: "base64",
|
|
808
|
-
commitment: "confirmed"
|
|
809
|
-
}).send();
|
|
810
|
-
if (!value) return null;
|
|
811
|
-
return decodeDrawBytes(address, new Uint8Array(base64.encode(value.data[0])));
|
|
812
|
-
}
|
|
813
|
-
/** Recover the final snapshot after automatic delivery closed the draw account. */
|
|
814
|
-
async function readDeliveredDraw(rpc, address) {
|
|
815
|
-
const signatures = await rpc.getSignaturesForAddress(address, {
|
|
816
|
-
commitment: "confirmed",
|
|
817
|
-
limit: 10
|
|
818
|
-
}).send();
|
|
819
|
-
const rows = await Promise.all(signatures.filter((row) => !row.err).map(async (row) => ({
|
|
820
|
-
slot: row.slot,
|
|
821
|
-
events: await readEvents(rpc, row.signature)
|
|
822
|
-
})));
|
|
823
|
-
const bought = rows.flatMap((row) => row.events.flatMap((event) => event.name === "PackBought" ? [{
|
|
824
|
-
slot: row.slot,
|
|
825
|
-
data: event.data
|
|
826
|
-
}] : []))[0];
|
|
827
|
-
const resolved = rows.flatMap((row) => row.events.flatMap((event) => event.name === "DrawResolved" ? [{
|
|
828
|
-
slot: row.slot,
|
|
829
|
-
data: event.data
|
|
830
|
-
}] : []))[0];
|
|
831
|
-
if (!bought || !resolved) return null;
|
|
832
|
-
if (bought.data.pool !== resolved.data.pool || bought.data.seq !== resolved.data.seq || bought.data.purchaser !== resolved.data.purchaser) return null;
|
|
833
|
-
const [derived, bump] = await findDrawPda({
|
|
834
|
-
pool: bought.data.pool,
|
|
835
|
-
seq: bought.data.seq
|
|
836
|
-
});
|
|
837
|
-
if (derived !== address) return null;
|
|
838
|
-
const ticketed = bought.data.prizes.filter((prize) => prize.tickets > 0);
|
|
839
|
-
if (ticketed.length === 0) return null;
|
|
840
|
-
const lastRetry = rows.flatMap((row) => row.events.flatMap((event) => event.name === "RandomnessRetried" && event.data.pool === bought.data.pool && event.data.seq === bought.data.seq ? [{
|
|
841
|
-
slot: row.slot,
|
|
842
|
-
attempt: event.data.attempt
|
|
843
|
-
}] : [])).reduce((latest, row) => !latest || row.slot > latest.slot ? row : latest, null);
|
|
844
|
-
return {
|
|
845
|
-
discriminator: DRAW_DISCRIMINATOR,
|
|
846
|
-
pool: bought.data.pool,
|
|
847
|
-
purchaser: bought.data.purchaser,
|
|
848
|
-
seq: bought.data.seq,
|
|
849
|
-
bump,
|
|
850
|
-
status: 1,
|
|
851
|
-
requestSlot: bought.slot,
|
|
852
|
-
lastAttemptSlot: lastRetry?.slot ?? bought.slot,
|
|
853
|
-
attempts: lastRetry?.attempt ?? 1,
|
|
854
|
-
maximum: ticketed.reduce((a, prize) => a > prize.amount ? a : prize.amount, 0n),
|
|
855
|
-
minimum: ticketed.reduce((a, prize) => a < prize.amount ? a : prize.amount, ticketed[0].amount),
|
|
856
|
-
prizes: bought.data.prizes,
|
|
857
|
-
amount: resolved.data.amount,
|
|
858
|
-
randomness: resolved.data.randomness,
|
|
859
|
-
timedOut: resolved.data.timedOut
|
|
860
|
-
};
|
|
581
|
+
return null;
|
|
861
582
|
}
|
|
862
583
|
//#endregion
|
|
863
584
|
//#region src/lookupTables.ts
|
|
864
585
|
/**
|
|
865
|
-
* Shared devnet address lookup table, verified on 2026-09-
|
|
586
|
+
* Shared devnet address lookup table, verified on 2026-09-18T07:26:38.218Z.
|
|
866
587
|
* Generated by scripts/deploy-lookup-table.ts. Existing indices are immutable;
|
|
867
588
|
* keep this table active while clients use it. Authority is the devnet deploy wallet.
|
|
868
589
|
*/
|
|
@@ -941,7 +662,47 @@ const DEVNET_LOOKUP_TABLE_ADDRESSES = [
|
|
|
941
662
|
address("Freijj9xKLefjrb5fHgT6KMbYG1XBP2mA83tqeXYUMYM"),
|
|
942
663
|
address("4uzPz9TPskXiiEZ6X78rqud8LvfdxkJBr5EKHgbx4azP"),
|
|
943
664
|
address("Hxzab4UjjVH2KjsdAqzdxGdYUpNN5FKhpu7iikB869uH"),
|
|
944
|
-
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")
|
|
945
706
|
];
|
|
946
707
|
const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET_LOOKUP_TABLE_ADDRESSES] };
|
|
947
708
|
/**
|
|
@@ -974,10 +735,10 @@ async function getOffer(client, mint, options = {}) {
|
|
|
974
735
|
}
|
|
975
736
|
/**
|
|
976
737
|
* The same computation with the reads already done. Useful when a caller holds a `ResolvedVenue`
|
|
977
|
-
* and wants to re-price without touching the network. `
|
|
738
|
+
* and wants to re-price without touching the network. `quoteAmount` is
|
|
978
739
|
* `venue.quoteBuy(pool.packTokens)`.
|
|
979
740
|
*/
|
|
980
|
-
function offerFromState(inventory, venue,
|
|
741
|
+
function offerFromState(inventory, venue, quoteAmount) {
|
|
981
742
|
const { pool } = inventory;
|
|
982
743
|
const tiers = tiersOf(pool);
|
|
983
744
|
const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
|
|
@@ -986,12 +747,10 @@ function offerFromState(inventory, venue, quoteLamports) {
|
|
|
986
747
|
mint: pool.mint,
|
|
987
748
|
pool: inventory.poolAddress,
|
|
988
749
|
packTokens: pool.packTokens,
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
seedLamports: pool.seedLamports,
|
|
994
|
-
seedTokens: pool.seedTokens,
|
|
750
|
+
quoteAmount,
|
|
751
|
+
quoteMint: pool.quoteMint,
|
|
752
|
+
seedQuoteAmount: pool.seedQuoteAmount,
|
|
753
|
+
seedTokens: seedTokens(pool.packTokens, tiers),
|
|
995
754
|
venue,
|
|
996
755
|
prizes: offer.prizes,
|
|
997
756
|
maximum: offer.maximum,
|
|
@@ -1078,8 +837,8 @@ function clusterNamedBy(url) {
|
|
|
1078
837
|
*/
|
|
1079
838
|
function assertClusterUrl(cluster, url) {
|
|
1080
839
|
const named = clusterNamedBy(url);
|
|
1081
|
-
if (cluster === "devnet" && named !== "devnet") throw new Error(`refusing to use ${url} as a devnet endpoint: it does not name devnet.\
|
|
1082
|
-
if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\
|
|
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.`);
|
|
1083
842
|
}
|
|
1084
843
|
/** `https://x` becomes `wss://x`, `http://x` becomes `ws://x`. Anything else is returned as is. */
|
|
1085
844
|
function websocketUrlFor(url) {
|
|
@@ -1111,339 +870,227 @@ function createClient(config) {
|
|
|
1111
870
|
};
|
|
1112
871
|
}
|
|
1113
872
|
//#endregion
|
|
1114
|
-
//#region src/tx/
|
|
873
|
+
//#region src/tx/wsol.ts
|
|
1115
874
|
/**
|
|
1116
|
-
*
|
|
875
|
+
* Wrapping and unwrapping SOL around a venue trade.
|
|
1117
876
|
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
*
|
|
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:
|
|
879
|
+
*
|
|
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
|
|
1124
889
|
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
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.
|
|
1127
894
|
*/
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
//#region src/referral.ts
|
|
1145
|
-
/** Resolve the purchaser's permanent referral binding for a pack purchase. */
|
|
1146
|
-
async function resolveReferral(client, purchaser, pool) {
|
|
1147
|
-
const link = await referralLinkAddress(purchaser);
|
|
1148
|
-
const account = await fetchMaybeReferralLink(client.rpc, link, { commitment: "confirmed" });
|
|
1149
|
-
if (!account.exists) return null;
|
|
1150
|
-
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 }));
|
|
1151
911
|
return {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
referral: await referralAddress(pool, referrer)
|
|
912
|
+
account,
|
|
913
|
+
instructions
|
|
1155
914
|
};
|
|
1156
915
|
}
|
|
1157
|
-
/**
|
|
1158
|
-
|
|
1159
|
-
return
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
addressLookupTables: options.addressLookupTables,
|
|
1164
|
-
computeUnitLimit: options.computeUnitLimit ?? 8e4,
|
|
1165
|
-
...options.computeUnitPrice === void 0 ? {} : { computeUnitPrice: options.computeUnitPrice }
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
/** Claim all accrued referral rewards for one pool. */
|
|
1169
|
-
async function claimReferral(client, referrer, pool, options = {}) {
|
|
1170
|
-
return await buildMessage(client, referrer, [await getClaimReferralInstructionAsync({
|
|
1171
|
-
referrer,
|
|
1172
|
-
pool
|
|
1173
|
-
})], {
|
|
1174
|
-
addressLookupTables: options.addressLookupTables,
|
|
1175
|
-
computeUnitLimit: options.computeUnitLimit ?? 8e4,
|
|
1176
|
-
...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
|
|
1177
922
|
});
|
|
1178
923
|
}
|
|
1179
924
|
//#endregion
|
|
1180
925
|
//#region src/tx/buyPack.ts
|
|
1181
|
-
/**
|
|
1182
|
-
* Buying a pack.
|
|
1183
|
-
*
|
|
1184
|
-
* One instruction does the whole thing: buy exactly `pool.packTokens` at the venue, move the
|
|
1185
|
-
* tokens into gabox custody, freeze a prize table into a new `Draw`, pay the creator's fee and
|
|
1186
|
-
* the protocol's 1% of the venue cost, and ask MagicBlock for randomness. Either all of it happens
|
|
1187
|
-
* or none of it does.
|
|
1188
|
-
*
|
|
1189
|
-
* # The three numbers the buyer signs
|
|
1190
|
-
*
|
|
1191
|
-
* `maxQuoteIn` is the slippage cap at the venue, in lamports (WSOL on PumpSwap). The token count
|
|
1192
|
-
* is fixed by the pool, so the cost is the only thing that moves. Set it from `offer.quoteLamports`
|
|
1193
|
-
* plus a margin, for example 2%.
|
|
1194
|
-
*
|
|
1195
|
-
* `minMaximum` is a floor on the top prize, in tokens. The table is fixed, so only an inventory
|
|
1196
|
-
* cap can lower it. Set it from `offer.maximum`.
|
|
1197
|
-
*
|
|
1198
|
-
* `maxTotalDebit` caps the venue debit plus the creator fee plus the protocol fee plus the VRF
|
|
1199
|
-
* request, measured in SOL and WSOL together. Both fees are a share of the venue debit, so budget
|
|
1200
|
-
* them from `maxQuoteIn`. It does **not** cover the rent Anchor pays for the `Draw` and any ATA it
|
|
1201
|
-
* creates, nor the transaction fee: the runtime charges those before the handler runs. Budget for
|
|
1202
|
-
* them separately.
|
|
1203
|
-
*
|
|
1204
|
-
* # PumpSwap needs WSOL first
|
|
1205
|
-
*
|
|
1206
|
-
* On the curve, Pump spends native SOL. On PumpSwap the quote is a classic WSOL token account, so
|
|
1207
|
-
* the buyer's WSOL ATA must exist and hold enough before the pack instruction runs. This builder
|
|
1208
|
-
* adds the create/fund/sync instructions when the route is PumpSwap. They are the buyer's own
|
|
1209
|
-
* instructions, signed by the buyer, and gabox never touches that account.
|
|
1210
|
-
*/
|
|
1211
926
|
async function buyPack(client, input) {
|
|
1212
|
-
const { mint, purchaser } = input;
|
|
1213
927
|
if (input.maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
1214
|
-
|
|
1215
|
-
|
|
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}`);
|
|
1216
931
|
const { pool, poolAddress } = inventory;
|
|
932
|
+
const purchaser = input.purchaser.address;
|
|
1217
933
|
const venue = await resolveVenue(client, {
|
|
1218
|
-
mint,
|
|
1219
|
-
user: purchaser
|
|
934
|
+
mint: input.mint,
|
|
935
|
+
user: purchaser,
|
|
1220
936
|
...input.venue ? { venue: input.venue } : {}
|
|
1221
937
|
});
|
|
1222
938
|
const draw = await drawAddress(poolAddress, input.seq ?? pool.nextSeq);
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (referral === null && input.referrer && canBind(purchaser.address, input.referrer)) {
|
|
1226
|
-
const referrer = input.referrer;
|
|
1227
|
-
bind = await getBindReferrerInstructionAsync({
|
|
1228
|
-
referee: purchaser,
|
|
1229
|
-
referrer
|
|
1230
|
-
});
|
|
1231
|
-
referral = {
|
|
1232
|
-
link: await referralLinkAddress(purchaser.address),
|
|
1233
|
-
referrer,
|
|
1234
|
-
referral: await referralAddress(poolAddress, referrer)
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
const buy = getBuyPackInstruction({
|
|
1238
|
-
purchaser,
|
|
939
|
+
const buy = await getBuyPackInstructionAsync({
|
|
940
|
+
purchaser: input.purchaser,
|
|
1239
941
|
pool: poolAddress,
|
|
1240
|
-
creator: pool.creator,
|
|
1241
942
|
draw,
|
|
1242
|
-
mint,
|
|
943
|
+
mint: input.mint,
|
|
944
|
+
quoteMint: pool.quoteMint,
|
|
1243
945
|
vault: pool.vault,
|
|
1244
|
-
userTokens: venueUserTokens(venue.buyAccounts, venue.kind),
|
|
1245
946
|
venue: venue.program,
|
|
1246
|
-
identity: await vrfIdentityAddress(),
|
|
1247
|
-
tokenProgram: pool.tokenProgram,
|
|
1248
947
|
maxQuoteIn: input.maxQuoteIn,
|
|
1249
948
|
minMaximum: input.minMaximum,
|
|
1250
|
-
|
|
1251
|
-
referralLink: referral?.link,
|
|
1252
|
-
referral: referral?.referral
|
|
949
|
+
maxNativeDebit: input.maxNativeDebit
|
|
1253
950
|
});
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
...
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
]
|
|
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, {
|
|
1260
958
|
addressLookupTables: input.addressLookupTables,
|
|
1261
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
959
|
+
computeUnitLimit: input.computeUnitLimit ?? 24e4,
|
|
1262
960
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1263
961
|
});
|
|
1264
962
|
}
|
|
1265
|
-
/** The default `Pubkey`: all zero bytes. `bind_referrer` refuses a link pointing at it. */
|
|
1266
|
-
const NO_REFERRER = "11111111111111111111111111111111";
|
|
1267
|
-
/**
|
|
1268
|
-
* The program refuses a self-referral and a link to the all-zero address. Both would take the
|
|
1269
|
-
* pack down with them, so the bind is skipped instead and the pack goes through unreferred.
|
|
1270
|
-
*/
|
|
1271
|
-
function canBind(purchaser, referrer) {
|
|
1272
|
-
return referrer !== purchaser && referrer !== NO_REFERRER;
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* The purchaser's base-token ATA, taken out of the venue account list rather than re-derived.
|
|
1276
|
-
*
|
|
1277
|
-
* `market.rs` requires `user_tokens` to equal the venue list's own `associated_base_user` (Pump) or
|
|
1278
|
-
* `user_base_token_account` (PumpSwap). Reading it back from the list makes the two agree by
|
|
1279
|
-
* construction instead of by two derivations that happen to match.
|
|
1280
|
-
*/
|
|
1281
|
-
function venueUserTokens(accounts, kind) {
|
|
1282
|
-
const account = accounts[kind === "pump" ? 14 : 5];
|
|
1283
|
-
if (!account) throw new Error(`the ${kind} account list is too short`);
|
|
1284
|
-
return account.address;
|
|
1285
|
-
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Create the buyer's WSOL account if needed, fund it, and sync its balance.
|
|
1288
|
-
*
|
|
1289
|
-
* `SyncNative` is the step people forget. A plain SOL transfer into a WSOL account raises its
|
|
1290
|
-
* lamports but not the `amount` field the token program reads, so the venue sees an empty account
|
|
1291
|
-
* until this instruction copies one to the other.
|
|
1292
|
-
*/
|
|
1293
|
-
function wsolPreparation$2(purchaser, lamports) {
|
|
1294
|
-
const wsol = wsolAccountFor(purchaser.address);
|
|
1295
|
-
return [
|
|
1296
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1297
|
-
payer: purchaser,
|
|
1298
|
-
ata: wsol,
|
|
1299
|
-
owner: purchaser.address,
|
|
1300
|
-
mint: WSOL_MINT,
|
|
1301
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1302
|
-
}),
|
|
1303
|
-
getTransferSolInstruction({
|
|
1304
|
-
source: purchaser,
|
|
1305
|
-
destination: wsol,
|
|
1306
|
-
amount: lamports
|
|
1307
|
-
}),
|
|
1308
|
-
getSyncNativeInstruction({ account: wsol })
|
|
1309
|
-
];
|
|
1310
|
-
}
|
|
1311
963
|
//#endregion
|
|
1312
964
|
//#region src/tx/createMachine.ts
|
|
1313
965
|
/**
|
|
1314
|
-
* Creating a machine: one transaction, two instructions, two signers.
|
|
1315
|
-
*
|
|
1316
|
-
* # Why it has to be one transaction
|
|
1317
|
-
*
|
|
1318
|
-
* `initialize_pool` reads the Instructions sysvar and refuses to run unless the same transaction
|
|
1319
|
-
* also carries a Pump `create_v2` for the same mint, signed by the same creator. That is what makes
|
|
1320
|
-
* "one pool per coin" true and stops anyone wrapping an existing coin in a machine. The create must
|
|
1321
|
-
* come **first**: the mint account has to exist and deserialize before Anchor validates
|
|
1322
|
-
* `initialize_pool`'s accounts.
|
|
1323
|
-
*
|
|
1324
|
-
* # The seed
|
|
1325
|
-
*
|
|
1326
|
-
* The creator owns none of the coin yet — it does not exist until this transaction runs. So
|
|
1327
|
-
* `initialize_pool` buys the seed on the curve itself, into the creator's own ATA, and moves it
|
|
1328
|
-
* straight into the vault. The Pump buy accounts ride along as `remainingAccounts`, in the same
|
|
1329
|
-
* order `buy_pack` uses.
|
|
1330
|
-
*
|
|
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.
|
|
1336
|
-
*
|
|
1337
|
-
* # Two signers
|
|
1338
|
-
*
|
|
1339
|
-
* The mint keypair signs `create_v2` — Pump takes it as a signer rather than deriving it — and the
|
|
1340
|
-
* creator signs both instructions and pays for everything.
|
|
1341
|
-
*/
|
|
1342
|
-
/**
|
|
1343
966
|
* Build the transaction message. Sign it with both `creator` and `mintKeypair`.
|
|
1344
967
|
*
|
|
1345
|
-
* Reads
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
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.
|
|
1348
971
|
*/
|
|
1349
972
|
async function createMachine(client, input) {
|
|
1350
|
-
const { creator, mintKeypair, name, symbol, uri,
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
973
|
+
const { creator, mintKeypair, name, symbol, uri, maxSeedQuoteIn, maxSeedNativeDebit } = input;
|
|
974
|
+
const tiers = cloneTiers(input.tiers ?? DEFAULT_TIERS);
|
|
975
|
+
validateTiers(tiers);
|
|
976
|
+
validatePack(PACK_TOKENS, tiers);
|
|
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);
|
|
1354
985
|
const mint = mintKeypair.address;
|
|
1355
|
-
const
|
|
986
|
+
const quoteMint = WSOL_MINT;
|
|
987
|
+
const create = await getLaunchInstruction({
|
|
1356
988
|
mint: mintKeypair,
|
|
1357
|
-
|
|
989
|
+
creator,
|
|
1358
990
|
name,
|
|
1359
991
|
symbol,
|
|
1360
|
-
uri
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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,
|
|
1366
1002
|
mint,
|
|
1003
|
+
quoteMint,
|
|
1004
|
+
baseVault: await launchlabVaultAddress(ids.launchlab, poolState, mint),
|
|
1005
|
+
quoteVault: await launchlabVaultAddress(ids.launchlab, poolState, quoteMint),
|
|
1367
1006
|
user: creator.address,
|
|
1368
|
-
|
|
1369
|
-
|
|
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)
|
|
1370
1011
|
});
|
|
1371
|
-
|
|
1012
|
+
const initialize = await getInitializePoolInstructionAsync({
|
|
1372
1013
|
creator,
|
|
1373
1014
|
mint,
|
|
1374
|
-
|
|
1375
|
-
venue:
|
|
1376
|
-
feeBps,
|
|
1015
|
+
quoteMint,
|
|
1016
|
+
venue: ids.launchlab,
|
|
1377
1017
|
tiers,
|
|
1378
|
-
|
|
1379
|
-
|
|
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, {
|
|
1380
1030
|
addressLookupTables: input.addressLookupTables,
|
|
1381
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
1031
|
+
computeUnitLimit: input.computeUnitLimit ?? 385e3,
|
|
1382
1032
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1383
1033
|
});
|
|
1384
1034
|
}
|
|
1385
1035
|
/**
|
|
1386
|
-
* What the seed for this
|
|
1036
|
+
* What the seed for this table costs, fees included, and how many tokens it is.
|
|
1037
|
+
*
|
|
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.
|
|
1387
1042
|
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
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.
|
|
1391
1046
|
*/
|
|
1392
|
-
async function seedCostEstimate(client,
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1047
|
+
async function seedCostEstimate(client, tiers = DEFAULT_TIERS, options = {}) {
|
|
1048
|
+
const copiedTiers = cloneTiers(tiers);
|
|
1049
|
+
validateTiers(copiedTiers);
|
|
1050
|
+
validatePack(PACK_TOKENS, copiedTiers);
|
|
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);
|
|
1395
1058
|
return {
|
|
1396
|
-
tiers,
|
|
1397
|
-
seedTokens:
|
|
1398
|
-
|
|
1059
|
+
tiers: copiedTiers,
|
|
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
|
|
1399
1065
|
};
|
|
1400
1066
|
}
|
|
1067
|
+
/** A mutable table shape, owned by this call and safe to pass to Codama's builder. */
|
|
1068
|
+
function cloneTiers(tiers) {
|
|
1069
|
+
return tiers.map(({ multiplierBps, tickets }) => ({
|
|
1070
|
+
multiplierBps,
|
|
1071
|
+
tickets
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1401
1074
|
/**
|
|
1402
|
-
* The
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
* `resolveVenue` cannot be used here. Everything the account list needs is known anyway: the
|
|
1406
|
-
* creator vault follows from the `creator` argument that `create_v2` records on the curve, and the
|
|
1407
|
-
* two fee recipients come from `Global`.
|
|
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.
|
|
1408
1078
|
*/
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
encoding: "base64",
|
|
1412
|
-
commitment: "confirmed"
|
|
1413
|
-
}).send();
|
|
1414
|
-
if (!value) throw new Error(`Pump's Global account is missing at ${PUMP_GLOBAL}`);
|
|
1415
|
-
const global = decodePumpGlobal(new Uint8Array(getBase64Encoder().encode(value.data[0])));
|
|
1416
|
-
const recipients = [global.feeRecipient, ...global.feeRecipients];
|
|
1417
|
-
const feeRecipient = recipients[options.feeRecipientIndex ?? randomIndex(recipients.length)];
|
|
1418
|
-
const buyback = global.buybackFeeRecipients[options.buybackRecipientIndex ?? randomIndex(global.buybackFeeRecipients.length)];
|
|
1419
|
-
if (!feeRecipient) throw new Error("Pump Global holds no fee recipient");
|
|
1420
|
-
if (!buyback) throw new Error("Pump Global holds no buyback fee recipient");
|
|
1421
|
-
return pumpBuyAccounts({
|
|
1422
|
-
mint: options.mint,
|
|
1423
|
-
user: options.user,
|
|
1424
|
-
tokenProgram: PUMP_CREATE_TOKEN_PROGRAM_ADDRESS,
|
|
1425
|
-
creator: options.user,
|
|
1426
|
-
feeRecipient,
|
|
1427
|
-
buybackFeeRecipient: buyback
|
|
1428
|
-
});
|
|
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`);
|
|
1429
1081
|
}
|
|
1430
|
-
const randomIndex = (length) => {
|
|
1431
|
-
if (length === 0) throw new Error("the list is empty");
|
|
1432
|
-
return Math.floor(Math.random() * length);
|
|
1433
|
-
};
|
|
1434
1082
|
//#endregion
|
|
1435
1083
|
//#region src/tx/draw.ts
|
|
1436
|
-
/** A retry and an expiry are both small. The default budget is plenty. */
|
|
1437
1084
|
const DRAW_COMPUTE_UNITS = 2e5;
|
|
1438
1085
|
async function retryDraw(client, input) {
|
|
1439
|
-
const
|
|
1086
|
+
const ix = await getRetryDrawInstruction({
|
|
1440
1087
|
payer: input.payer,
|
|
1441
1088
|
pool: input.pool,
|
|
1442
1089
|
draw: input.draw,
|
|
1443
1090
|
identity: await vrfIdentityAddress(),
|
|
1444
1091
|
maxVrfDebit: input.maxVrfDebit
|
|
1445
1092
|
});
|
|
1446
|
-
return await buildMessage(client, input.payer, [
|
|
1093
|
+
return await buildMessage(client, input.payer, [ix], {
|
|
1447
1094
|
addressLookupTables: input.addressLookupTables,
|
|
1448
1095
|
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1449
1096
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
@@ -1451,295 +1098,94 @@ async function retryDraw(client, input) {
|
|
|
1451
1098
|
}
|
|
1452
1099
|
async function expireDraw(client, input) {
|
|
1453
1100
|
const draw = await fetchDraw(client, input.draw);
|
|
1454
|
-
if (!draw) throw new Error(
|
|
1455
|
-
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");
|
|
1456
1102
|
const pool = await fetchPoolAt(client, input.pool);
|
|
1457
1103
|
if (!pool) throw new Error(`no pool at ${input.pool}`);
|
|
1458
|
-
const
|
|
1104
|
+
const ix = getExpireDrawInstruction({
|
|
1459
1105
|
pool: input.pool,
|
|
1460
1106
|
draw: input.draw,
|
|
1461
1107
|
purchaser: draw.purchaser,
|
|
1462
1108
|
mint: pool.mint,
|
|
1463
1109
|
vault: pool.vault,
|
|
1464
|
-
userTokens: await associatedTokenAddress(draw.purchaser, pool.mint
|
|
1465
|
-
tokenProgram: pool.tokenProgram
|
|
1110
|
+
userTokens: await associatedTokenAddress(draw.purchaser, pool.mint)
|
|
1466
1111
|
});
|
|
1467
|
-
return await buildMessage(client, input.payer, [
|
|
1112
|
+
return await buildMessage(client, input.payer, [ix], {
|
|
1468
1113
|
addressLookupTables: input.addressLookupTables,
|
|
1469
1114
|
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1470
1115
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1471
1116
|
});
|
|
1472
1117
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
*/
|
|
1480
|
-
async function drawAvailability(client, draw) {
|
|
1481
|
-
const record = await fetchDraw(client, draw);
|
|
1482
|
-
if (!record) return null;
|
|
1483
|
-
const slot = await client.rpc.getSlot({ commitment: "confirmed" }).send();
|
|
1484
|
-
const now = BigInt(slot);
|
|
1485
|
-
const retryAt = record.lastAttemptSlot + RETRY_SLOTS;
|
|
1486
|
-
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;
|
|
1487
1124
|
const slotsUntilRetry = now >= retryAt ? 0n : retryAt - now;
|
|
1488
1125
|
const slotsUntilExpiry = now >= expireAt ? 0n : expireAt - now;
|
|
1489
|
-
const pending = record.status === 0;
|
|
1490
1126
|
return {
|
|
1491
|
-
|
|
1492
|
-
attempts: record.attempts,
|
|
1127
|
+
attempts: draw.attempts,
|
|
1493
1128
|
slotsUntilRetry,
|
|
1494
1129
|
slotsUntilExpiry,
|
|
1495
|
-
canRetry:
|
|
1496
|
-
canExpire:
|
|
1130
|
+
canRetry: draw.attempts < 3 && slotsUntilRetry === 0n && slotsUntilExpiry > 0n,
|
|
1131
|
+
canExpire: slotsUntilExpiry === 0n
|
|
1497
1132
|
};
|
|
1498
1133
|
}
|
|
1499
1134
|
//#endregion
|
|
1500
1135
|
//#region src/tx/fundPrizes.ts
|
|
1501
|
-
/**
|
|
1502
|
-
* Donating prize inventory.
|
|
1503
|
-
*
|
|
1504
|
-
* `fund_prizes` moves tokens from any wallet into the pool's vault. It is a donation and it is
|
|
1505
|
-
* irrevocable: there is no withdrawal instruction, and no authority can move vault tokens. The only
|
|
1506
|
-
* way out of the vault is prize delivery.
|
|
1507
|
-
*
|
|
1508
|
-
* Funding raises what later packs can pay. It does not change the tiers, which are immutable, and
|
|
1509
|
-
* it does not change any draw that is already frozen — `math.quote` is computed at purchase time
|
|
1510
|
-
* and stored in the `Draw`.
|
|
1511
|
-
*/
|
|
1512
1136
|
const FUND_COMPUTE_UNITS = 2e5;
|
|
1513
|
-
/** A venue buy plus the vault transfer. The buy is the expensive half. */
|
|
1514
|
-
const FUND_WITH_BUY_COMPUTE_UNITS = 4e5;
|
|
1515
1137
|
async function fundPrizes(client, input) {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
mint,
|
|
1524
|
-
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),
|
|
1525
1146
|
vault: pool.vault,
|
|
1526
|
-
|
|
1527
|
-
amount
|
|
1528
|
-
})], {
|
|
1529
|
-
addressLookupTables: input.addressLookupTables,
|
|
1530
|
-
computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
|
|
1531
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1147
|
+
amount: input.amount
|
|
1532
1148
|
});
|
|
1533
|
-
|
|
1534
|
-
/**
|
|
1535
|
-
* Buy tokens at the venue with SOL and donate them to the vault, in one transaction.
|
|
1536
|
-
*
|
|
1537
|
-
* This is what a donor with SOL and no coins needs. `fundPrizes` moves tokens the donor already
|
|
1538
|
-
* holds; this one buys them first. Both are irrevocable — there is no withdrawal instruction, and
|
|
1539
|
-
* no authority can move vault tokens.
|
|
1540
|
-
*
|
|
1541
|
-
* Three instructions on the curve route: create the donor's token account if it is missing, buy,
|
|
1542
|
-
* donate. Pump creates the account itself, but the idempotent instruction costs nothing when it
|
|
1543
|
-
* already exists and it makes the transaction correct on its own terms. The PumpSwap route adds the
|
|
1544
|
-
* WSOL create/fund/sync prefix, for the same reason `buyPack` does: PumpSwap spends WSOL.
|
|
1545
|
-
*/
|
|
1546
|
-
async function fundPrizesWithBuy(client, input) {
|
|
1547
|
-
const { mint, funder, tokens, maxQuoteIn } = input;
|
|
1548
|
-
if (tokens <= 0n) throw new Error("tokens must be positive");
|
|
1549
|
-
if (maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
1550
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1551
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1552
|
-
const venue = await resolveVenue(client, {
|
|
1553
|
-
mint,
|
|
1554
|
-
user: funder.address,
|
|
1555
|
-
...input.venue ? { venue: input.venue } : {}
|
|
1556
|
-
});
|
|
1557
|
-
const source = await associatedTokenAddress(funder.address, mint, pool.tokenProgram);
|
|
1558
|
-
return await buildMessage(client, funder, [
|
|
1559
|
-
...venue.kind === "pumpswap" ? wsolPreparation$1(funder, input.wrapLamports ?? maxQuoteIn) : [],
|
|
1560
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1561
|
-
payer: funder,
|
|
1562
|
-
ata: source,
|
|
1563
|
-
owner: funder.address,
|
|
1564
|
-
mint,
|
|
1565
|
-
tokenProgram: pool.tokenProgram
|
|
1566
|
-
}),
|
|
1567
|
-
venueBuyInstruction({
|
|
1568
|
-
venue,
|
|
1569
|
-
user: funder,
|
|
1570
|
-
tokens,
|
|
1571
|
-
maxQuoteIn
|
|
1572
|
-
}),
|
|
1573
|
-
getFundPrizesInstruction({
|
|
1574
|
-
funder,
|
|
1575
|
-
pool: await poolAddress(mint),
|
|
1576
|
-
mint,
|
|
1577
|
-
source,
|
|
1578
|
-
vault: pool.vault,
|
|
1579
|
-
tokenProgram: pool.tokenProgram,
|
|
1580
|
-
amount: tokens
|
|
1581
|
-
})
|
|
1582
|
-
], {
|
|
1149
|
+
return await buildMessage(client, input.funder, [ix], {
|
|
1583
1150
|
addressLookupTables: input.addressLookupTables,
|
|
1584
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
1151
|
+
computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
|
|
1585
1152
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1586
1153
|
});
|
|
1587
1154
|
}
|
|
1588
|
-
/** Create the donor's WSOL account if needed, fund it, and sync its balance. See `buyPack.ts`. */
|
|
1589
|
-
function wsolPreparation$1(funder, lamports) {
|
|
1590
|
-
const wsol = wsolAccountFor(funder.address);
|
|
1591
|
-
return [
|
|
1592
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1593
|
-
payer: funder,
|
|
1594
|
-
ata: wsol,
|
|
1595
|
-
owner: funder.address,
|
|
1596
|
-
mint: WSOL_MINT,
|
|
1597
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1598
|
-
}),
|
|
1599
|
-
getTransferSolInstruction({
|
|
1600
|
-
source: funder,
|
|
1601
|
-
destination: wsol,
|
|
1602
|
-
amount: lamports
|
|
1603
|
-
}),
|
|
1604
|
-
getSyncNativeInstruction({ account: wsol })
|
|
1605
|
-
];
|
|
1606
|
-
}
|
|
1607
1155
|
//#endregion
|
|
1608
1156
|
//#region src/tx/redeem.ts
|
|
1609
|
-
/**
|
|
1610
|
-
* Selling an automatically delivered prize, plus settlement helpers for legacy resolve-only draws.
|
|
1611
|
-
*
|
|
1612
|
-
* Every sale here goes through the program (`sell_tokens` or `sell_prize`), which does the venue
|
|
1613
|
-
* CPI and then pays the protocol 1% of what the sale returned. On Pump the proceeds and the fee are
|
|
1614
|
-
* native SOL. On PumpSwap both are WSOL: the proceeds land in the seller's WSOL account and the fee
|
|
1615
|
-
* moves from there to the collector's WSOL account. Add an unwrap afterwards if the seller wants
|
|
1616
|
-
* lamports.
|
|
1617
|
-
*
|
|
1618
|
-
* `minQuoteOutput` is the venue's own floor, checked before the fee. The seller keeps 99% of
|
|
1619
|
-
* whatever the venue paid above it.
|
|
1620
|
-
*/
|
|
1621
|
-
/**
|
|
1622
|
-
* Sell a prize that the VRF callback already delivered to the wallet, through `sell_tokens`.
|
|
1623
|
-
*
|
|
1624
|
-
* The program needs the coin's pool: it keeps the vault out of the venue's account list. A mint
|
|
1625
|
-
* with no pool cannot be sold this way; use a plain venue trade for that.
|
|
1626
|
-
*/
|
|
1627
1157
|
async function sellTokens(client, input) {
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
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}`);
|
|
1633
1162
|
const venue = await resolveVenue(client, {
|
|
1634
|
-
mint,
|
|
1635
|
-
user: seller.address,
|
|
1163
|
+
mint: input.mint,
|
|
1164
|
+
user: input.seller.address,
|
|
1636
1165
|
...input.venue ? { venue: input.venue } : {}
|
|
1637
1166
|
});
|
|
1638
|
-
const sell =
|
|
1639
|
-
seller,
|
|
1640
|
-
pool: await poolAddress(mint),
|
|
1641
|
-
mint,
|
|
1642
|
-
|
|
1167
|
+
const sell = await getSellTokensInstructionAsync({
|
|
1168
|
+
seller: input.seller,
|
|
1169
|
+
pool: await poolAddress(input.mint),
|
|
1170
|
+
mint: input.mint,
|
|
1171
|
+
quoteMint: pool.quoteMint,
|
|
1643
1172
|
venue: venue.program,
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
minQuoteOutput
|
|
1648
|
-
});
|
|
1649
|
-
return await buildMessage(client, seller, [...wsolPreparation(seller, venue), withRemainingAccounts(sell, venue.sellAccounts)], {
|
|
1650
|
-
addressLookupTables: input.addressLookupTables,
|
|
1651
|
-
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1652
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
/** Transfer a legacy resolve-only award into the purchaser's token account and close the draw. */
|
|
1656
|
-
async function claimPrize(client, input) {
|
|
1657
|
-
const { mint, purchaser, draw } = input;
|
|
1658
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1659
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1660
|
-
return await buildMessage(client, purchaser, [getClaimPrizeInstruction({
|
|
1661
|
-
purchaser,
|
|
1662
|
-
pool: await poolAddress(mint),
|
|
1663
|
-
draw,
|
|
1664
|
-
mint,
|
|
1665
|
-
vault: pool.vault,
|
|
1666
|
-
userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
|
|
1667
|
-
tokenProgram: pool.tokenProgram
|
|
1668
|
-
})], {
|
|
1669
|
-
addressLookupTables: input.addressLookupTables,
|
|
1670
|
-
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1671
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
/**
|
|
1675
|
-
* Claim and sell a legacy resolve-only draw in one transaction.
|
|
1676
|
-
*
|
|
1677
|
-
* The award is read off the draw so the caller can price the sale before signing. A draw that has
|
|
1678
|
-
* not resolved has no award yet, and this throws rather than building a sale of zero tokens.
|
|
1679
|
-
*/
|
|
1680
|
-
async function sellPrize(client, input) {
|
|
1681
|
-
const { mint, purchaser, draw, minQuoteOutput } = input;
|
|
1682
|
-
if (minQuoteOutput <= 0n) throw new Error("minQuoteOutput must be positive");
|
|
1683
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1684
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1685
|
-
const record = await fetchDraw(client, draw);
|
|
1686
|
-
if (!record) throw new Error(`no draw at ${draw} — it may already be redeemed`);
|
|
1687
|
-
if (record.amount === 0n) throw new Error(`draw ${draw} has no award yet; it has not resolved`);
|
|
1688
|
-
const venue = await resolveVenue(client, {
|
|
1689
|
-
mint,
|
|
1690
|
-
user: purchaser.address,
|
|
1691
|
-
...input.venue ? { venue: input.venue } : {}
|
|
1173
|
+
amount: input.amount,
|
|
1174
|
+
minQuoteOutput: input.minQuoteOutput,
|
|
1175
|
+
maxNativeDebit: input.maxNativeDebit
|
|
1692
1176
|
});
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
tokenProgram: pool.tokenProgram,
|
|
1701
|
-
venue: venue.program,
|
|
1702
|
-
feeCollectorWsol: await feeCollectorWsolAddress(),
|
|
1703
|
-
minQuoteOutput
|
|
1704
|
-
});
|
|
1705
|
-
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, {
|
|
1706
1184
|
addressLookupTables: input.addressLookupTables,
|
|
1707
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
1185
|
+
computeUnitLimit: input.computeUnitLimit ?? 135e3,
|
|
1708
1186
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1709
1187
|
});
|
|
1710
1188
|
}
|
|
1711
|
-
/**
|
|
1712
|
-
* On PumpSwap the proceeds land in the seller's WSOL account, so it has to exist before the sale.
|
|
1713
|
-
* Pump's own SDK does the same before its sells. The instruction is idempotent and the account is
|
|
1714
|
-
* the seller's own; gabox never touches it. Pump pays native SOL, so the curve route adds nothing.
|
|
1715
|
-
*/
|
|
1716
|
-
function wsolPreparation(seller, venue) {
|
|
1717
|
-
if (venue.kind !== "pumpswap") return [];
|
|
1718
|
-
return [getCreateAssociatedTokenIdempotentInstruction({
|
|
1719
|
-
payer: seller,
|
|
1720
|
-
ata: wsolAccountFor(seller.address),
|
|
1721
|
-
owner: seller.address,
|
|
1722
|
-
mint: WSOL_MINT,
|
|
1723
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1724
|
-
})];
|
|
1725
|
-
}
|
|
1726
|
-
/**
|
|
1727
|
-
* Quote the award held by a legacy open draw before the seller signs a floor.
|
|
1728
|
-
*
|
|
1729
|
-
* Read the draw, ask the venue, and subtract your own slippage tolerance to get `minQuoteOutput`.
|
|
1730
|
-
*/
|
|
1731
|
-
async function quoteSellPrize(client, mint, draw, user) {
|
|
1732
|
-
const record = await fetchDraw(client, draw);
|
|
1733
|
-
if (!record) throw new Error(`no draw at ${draw}`);
|
|
1734
|
-
const venue = await resolveVenue(client, {
|
|
1735
|
-
mint,
|
|
1736
|
-
user
|
|
1737
|
-
});
|
|
1738
|
-
return {
|
|
1739
|
-
award: record.amount,
|
|
1740
|
-
grossOutput: venue.quoteSell(record.amount)
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
1189
|
//#endregion
|
|
1744
1190
|
//#region src/vrf.ts
|
|
1745
1191
|
/**
|
|
@@ -1758,6 +1204,6 @@ async function oracleAccounts() {
|
|
|
1758
1204
|
};
|
|
1759
1205
|
}
|
|
1760
1206
|
//#endregion
|
|
1761
|
-
export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
|
|
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 };
|
|
1762
1208
|
|
|
1763
1209
|
//# sourceMappingURL=index.js.map
|