@gabox-labs/sdk 0.1.1
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 +37 -0
- package/LICENSE +21 -0
- package/README.md +511 -0
- package/dist/gaboxV2-CV2XltqC.js +3347 -0
- package/dist/gaboxV2-CV2XltqC.js.map +1 -0
- package/dist/generated/index.d.ts +1468 -0
- package/dist/generated/index.js +795 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index-BxvSkzCO.d.ts +471 -0
- package/dist/index.d.ts +823 -0
- package/dist/index.js +1763 -0
- package/dist/index.js.map +1 -0
- package/dist/pump/index.d.ts +2 -0
- package/dist/pump/index.js +2 -0
- package/dist/pump-D0K_0uiC.js +1531 -0
- package/dist/pump-D0K_0uiC.js.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/llms.txt +22 -0
- package/package.json +97 -0
- package/skills/gabox-sdk/SKILL.md +163 -0
- package/skills/gabox-sdk/references/api.md +149 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1763 @@
|
|
|
1
|
+
import { $t as findPoolPda, Dn as decodePool, En as POOL_DISCRIMINATOR, Hn as fetchMaybeDraw, In as getPoolSize, Kt as getBindReferrerInstructionAsync, L as getInitializePoolInstructionAsync, Ln as DRAW_DISCRIMINATOR, Lt as getBuyPackInstruction, O as getRetryDrawInstruction, Qt as findReferralPda, Rn as decodeDraw, St as getClaimReferralInstructionAsync, W as getFundPrizesInstruction, Zt as findReferralLinkPda, an as decodeReferralLink, at as getExpireDrawInstruction, b as getSellPrizeInstruction, cn as fetchMaybeReferralLink, f as getSellTokensInstruction, in as REFERRAL_LINK_DISCRIMINATOR, kt as getClaimPrizeInstruction, mn as getReferralLinkSize, nn as findDrawPda, tn as findIdentityPda, yn as fetchMaybeReferral } from "./gaboxV2-CV2XltqC.js";
|
|
2
|
+
import { $ as MAX_ATTEMPTS, Q as INSTRUCTIONS_SYSVAR, T as PUMP_GLOBAL, U as decodePumpGlobal, X as GABOX_PROGRAM_ID, Z as IDENTITY_SEED, at as RETRY_SLOTS, ct as TOKEN_2022_PROGRAM_ADDRESS, d as pumpBuyAccounts, dt as VRF_PROGRAM_ADDRESS, et as MAX_FEE_BPS, ft as WSOL_MINT, gt as PUMP_CREATE_TOKEN_PROGRAM_ADDRESS, h as swapCashbackAccount, i as getPumpCreateV2Instruction, it as REFERRAL_FEE_BPS, l as tokenAccountAmount, lt as TOKEN_PROGRAM_ADDRESS, mt as MAYHEM_PROGRAM_ADDRESS, n as venueBuyInstruction, nt as PROTOCOL_FEE_BPS, o as newCurveBuyCost, ot as SLOT_HASHES_SYSVAR, pt as ASSOCIATED_TOKEN_PROGRAM_ADDRESS, rt as PROTOCOL_FEE_COLLECTOR, s as resolveVenue, st as TIMEOUT_SLOTS, t as pump_exports, tt as PACK_TOKENS, u as wsolAccountFor, ut as VRF_DEFAULT_QUEUE, vt as PUMP_FEE_PROGRAM_ADDRESS, wt as SYSTEM_PROGRAM_ADDRESS, xt as PUMP_SWAP_PROGRAM_ADDRESS, yt as PUMP_PROGRAM_ADDRESS } from "./pump-D0K_0uiC.js";
|
|
3
|
+
import { DRAW_RESOLVED_EVENT_DISCRIMINATOR, PACK_BOUGHT_EVENT_DISCRIMINATOR, POOL_CREATED_EVENT_DISCRIMINATOR, PRIZES_FUNDED_EVENT_DISCRIMINATOR, PRIZE_REDEEMED_EVENT_DISCRIMINATOR, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR, TOKENS_SOLD_EVENT_DISCRIMINATOR, getDrawResolvedEventDecoder, getPackBoughtEventDecoder, getPoolCreatedEventDecoder, getPrizeRedeemedEventDecoder, getPrizesFundedEventDecoder, getRandomnessRetriedEventDecoder, getTokensSoldEventDecoder, t as generated_exports } from "./generated/index.js";
|
|
4
|
+
import { address, appendTransactionMessageInstructions, compileTransaction, compressTransactionMessageUsingAddressLookupTables, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, fetchEncodedAccount, fetchEncodedAccounts, getAddressEncoder, getBase58Decoder, getBase64Encoder, getBytesEncoder, getProgramDerivedAddress, getStructEncoder, getTransactionEncoder, getU32Encoder, getU64Encoder, getU8Encoder, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
|
|
5
|
+
import { TOKEN_PROGRAM_ADDRESS as TOKEN_PROGRAM_ADDRESS$1, getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
|
|
6
|
+
import { getTransferSolInstruction } from "@solana-program/system";
|
|
7
|
+
//#region src/math.ts
|
|
8
|
+
/**
|
|
9
|
+
* A bigint port of `programs/gabox-v2/src/math.rs`.
|
|
10
|
+
*
|
|
11
|
+
* The point is that a client can show a buyer the exact prize table the program will freeze into
|
|
12
|
+
* their `Draw`, before they pay. Every rounding step here matches the Rust, including the direction
|
|
13
|
+
* of each division. `test/math.test.ts` runs the same vectors as `programs/gabox-v2/tests/
|
|
14
|
+
* economics.rs`.
|
|
15
|
+
*
|
|
16
|
+
* All amounts are `bigint`, in the mint's smallest unit. `multiplierBps` and `tickets` are numbers
|
|
17
|
+
* because both are `u32` in the program and both stay small.
|
|
18
|
+
*/
|
|
19
|
+
/** Basis points. A multiplier of 10_000 pays back exactly one pack. */
|
|
20
|
+
const BPS = 10000n;
|
|
21
|
+
/** `math::share`. `bps` basis points of `amount`, rounded down. */
|
|
22
|
+
function share(amount, bps) {
|
|
23
|
+
return amount * bps / BPS;
|
|
24
|
+
}
|
|
25
|
+
/** Ticket counts must sum to this. A uniform 16-bit word then maps with no modulo bias. */
|
|
26
|
+
const TICKETS = 65536;
|
|
27
|
+
/** The prize table has exactly this many slots. Unused slots are all-zero. */
|
|
28
|
+
const TIERS = 8;
|
|
29
|
+
/** Thrown by every function here. `code` matches a `GaboxError` variant name. */
|
|
30
|
+
var GaboxMathError = class extends Error {
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(`${code}: ${message}`);
|
|
34
|
+
this.name = "GaboxMathError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
39
|
+
const U32_MAX = (1n << 32n) - 1n;
|
|
40
|
+
/**
|
|
41
|
+
* All profiles target a 95% token return. These are slices of one pack's expected-value budget
|
|
42
|
+
* assigned to Rare, Epic, and Mythic respectively. A riskier profile moves more of that budget
|
|
43
|
+
* out of Common and into every higher rarity; the ticket counts still decrease as rarity rises.
|
|
44
|
+
*/
|
|
45
|
+
const RISK_PROFILES = {
|
|
46
|
+
conservative: {
|
|
47
|
+
rarityEvBps: [
|
|
48
|
+
1000n,
|
|
49
|
+
600n,
|
|
50
|
+
300n
|
|
51
|
+
],
|
|
52
|
+
targetEvBps: 9500n
|
|
53
|
+
},
|
|
54
|
+
balanced: {
|
|
55
|
+
rarityEvBps: [
|
|
56
|
+
1400n,
|
|
57
|
+
1000n,
|
|
58
|
+
700n
|
|
59
|
+
],
|
|
60
|
+
targetEvBps: 9500n
|
|
61
|
+
},
|
|
62
|
+
jackpot: {
|
|
63
|
+
rarityEvBps: [
|
|
64
|
+
1800n,
|
|
65
|
+
1500n,
|
|
66
|
+
1200n
|
|
67
|
+
],
|
|
68
|
+
targetEvBps: 9500n
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function checkedU64(value, what) {
|
|
72
|
+
if (value < 0n || value > U64_MAX) throw new GaboxMathError("Arithmetic", `${what} does not fit in u64`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
/** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
|
|
76
|
+
function tierAmount(base, multiplierBps) {
|
|
77
|
+
return checkedU64(base * BigInt(multiplierBps) / BPS, "tier amount");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `math::validate`. Checks the table alone, with no base.
|
|
81
|
+
*
|
|
82
|
+
* Three rules, and each one closes a way to sell a bad ticket:
|
|
83
|
+
* - ticket counts sum to exactly 65,536, so the 16-bit draw is uniform;
|
|
84
|
+
* - an unused row is all-zero, so a hidden multiplier cannot ride along;
|
|
85
|
+
* - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
|
|
86
|
+
* tokens than a pack buys. This bounds tokens, not cash value.
|
|
87
|
+
*/
|
|
88
|
+
function validateTiers(tiers) {
|
|
89
|
+
if (tiers.length !== 8) throw new GaboxMathError("InvalidDistribution", `expected 8 tiers, got ${tiers.length}`);
|
|
90
|
+
let count = 0n;
|
|
91
|
+
let expected = 0n;
|
|
92
|
+
for (const tier of tiers) {
|
|
93
|
+
if (tier.tickets === 0) {
|
|
94
|
+
if (tier.multiplierBps !== 0) throw new GaboxMathError("InvalidDistribution", "a tier with no tickets must have no multiplier");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (tier.multiplierBps <= 0) throw new GaboxMathError("InvalidDistribution", "a ticketed tier must pay something; it would sell a ticket that can only win zero");
|
|
98
|
+
count += BigInt(tier.tickets);
|
|
99
|
+
expected += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
100
|
+
}
|
|
101
|
+
if (count !== BigInt(65536)) throw new GaboxMathError("InvalidDistribution", `tickets must sum to ${TICKETS}, they sum to ${count}`);
|
|
102
|
+
if (expected > 10000n * BigInt(65536)) throw new GaboxMathError("UnfundedExpectation", "the expected token award exceeds the tokens a pack buys");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `math::validate_pack`. Every ticketed tier must pay at least one token for a pack of this size.
|
|
106
|
+
*
|
|
107
|
+
* The program checks this once at creation. At `PACK_TOKENS` no sane table fails it; it exists so
|
|
108
|
+
* a table cannot sell a ticket that can only win zero.
|
|
109
|
+
*/
|
|
110
|
+
function validatePack(packTokens, tiers) {
|
|
111
|
+
if (packTokens <= 0n) throw new GaboxMathError("ZeroAmount", "packTokens must be positive");
|
|
112
|
+
checkedU64(packTokens, "pack tokens");
|
|
113
|
+
for (const [i, tier] of tiers.entries()) {
|
|
114
|
+
if (tier.tickets === 0) continue;
|
|
115
|
+
if (tierAmount(packTokens, tier.multiplierBps) === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `math::seed_tokens`. The seed a table needs, in tokens.
|
|
120
|
+
*
|
|
121
|
+
* The first pack must be able to pay the largest tier in full. The pack itself brings `packTokens`
|
|
122
|
+
* into the vault, so the vault has to hold the rest beforehand:
|
|
123
|
+
*
|
|
124
|
+
* seedTokens = uncappedMaximum(packTokens, tiers) - packTokens
|
|
125
|
+
*
|
|
126
|
+
* A 5x jackpot on a 1M-token pack needs a 4M-token seed. A table whose top tier pays exactly one
|
|
127
|
+
* pack needs no seed. A top tier below one pack is refused: every ticket would lose.
|
|
128
|
+
*
|
|
129
|
+
* `initialize_pool` computes this number itself and buys exactly that many tokens on the curve.
|
|
130
|
+
* The creator only signs a maximum SOL cost.
|
|
131
|
+
*/
|
|
132
|
+
function seedTokens(packTokens, tiers) {
|
|
133
|
+
const largest = uncappedMaximum(packTokens, tiers);
|
|
134
|
+
if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
|
|
135
|
+
return largest - packTokens;
|
|
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;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
|
|
187
|
+
*
|
|
188
|
+
* At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
|
|
189
|
+
*/
|
|
190
|
+
function uncappedMaximum(base, tiers) {
|
|
191
|
+
let largest = 0n;
|
|
192
|
+
for (const tier of tiers) {
|
|
193
|
+
if (tier.tickets === 0) continue;
|
|
194
|
+
const amount = tierAmount(base, tier.multiplierBps);
|
|
195
|
+
if (amount > largest) largest = amount;
|
|
196
|
+
}
|
|
197
|
+
return largest;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* `math::quote`. The offer a pack of `base` tokens would freeze right now. `base` is always
|
|
201
|
+
* `pool.packTokens`; the parameter stays general so the vectors can use small numbers.
|
|
202
|
+
*
|
|
203
|
+
* `inventory` is the vault's token balance and `reserved` is `pool.reserved`. The difference is
|
|
204
|
+
* free inventory. This pack's own `base` is added to it, because the purchase and the offer are
|
|
205
|
+
* one transaction — the tokens are in the vault before the draw is written.
|
|
206
|
+
*
|
|
207
|
+
* Every amount is capped at what is actually available. A tier that rounds to zero tokens is an
|
|
208
|
+
* error: the pool must not sell a ticket that can only win nothing.
|
|
209
|
+
*/
|
|
210
|
+
function quote(base, tiers, inventory, reserved) {
|
|
211
|
+
if (inventory < reserved) throw new GaboxMathError("InsolventInventory", "the vault holds less than the pool has already reserved");
|
|
212
|
+
const available = checkedU64(inventory - reserved + base, "available inventory");
|
|
213
|
+
const prizes = Array.from({ length: 8 }, () => ({
|
|
214
|
+
amount: 0n,
|
|
215
|
+
tickets: 0
|
|
216
|
+
}));
|
|
217
|
+
let maximum = 0n;
|
|
218
|
+
let minimum = U64_MAX;
|
|
219
|
+
for (const [i, tier] of tiers.entries()) {
|
|
220
|
+
if (tier.tickets === 0) continue;
|
|
221
|
+
const uncapped = tierAmount(base, tier.multiplierBps);
|
|
222
|
+
if (uncapped === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
223
|
+
const amount = uncapped < available ? uncapped : available;
|
|
224
|
+
prizes[i] = {
|
|
225
|
+
amount,
|
|
226
|
+
tickets: tier.tickets
|
|
227
|
+
};
|
|
228
|
+
if (amount > maximum) maximum = amount;
|
|
229
|
+
if (amount < minimum) minimum = amount;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
prizes,
|
|
233
|
+
maximum,
|
|
234
|
+
minimum
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `math::choose`. Which prize a 16-bit ticket wins.
|
|
239
|
+
*
|
|
240
|
+
* The rows are consecutive ranges in table order, so this is a running total and a comparison.
|
|
241
|
+
* A ticket past the last row returns `0`, which cannot happen for a validated table.
|
|
242
|
+
*/
|
|
243
|
+
function choose(prizes, ticket) {
|
|
244
|
+
let end = 0;
|
|
245
|
+
for (const prize of prizes) {
|
|
246
|
+
end += prize.tickets;
|
|
247
|
+
if (ticket < end) return prize.amount;
|
|
248
|
+
}
|
|
249
|
+
return 0n;
|
|
250
|
+
}
|
|
251
|
+
/** `math::resolve_reservation`. Release the unwon part of a maximum, keep the award reserved. */
|
|
252
|
+
function resolveReservation(reserved, maximum, award) {
|
|
253
|
+
if (award > maximum) throw new GaboxMathError("InsolventInventory", "the award exceeds the reserved maximum");
|
|
254
|
+
if (reserved < maximum) throw new GaboxMathError("Arithmetic", "the pool has reserved less than this draw holds");
|
|
255
|
+
return checkedU64(reserved - maximum + award, "reserved");
|
|
256
|
+
}
|
|
257
|
+
/** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
|
|
258
|
+
function maxMultiplierBps(tiers) {
|
|
259
|
+
let largest = 0;
|
|
260
|
+
for (const tier of tiers) {
|
|
261
|
+
if (tier.tickets === 0) continue;
|
|
262
|
+
if (tier.multiplierBps > largest) largest = tier.multiplierBps;
|
|
263
|
+
}
|
|
264
|
+
return largest;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The expected multiplier over all 65,536 tickets, in basis points. Rounded down.
|
|
268
|
+
*
|
|
269
|
+
* `validateTiers` caps this at 10,000. A table at 9,800 keeps 2% of every pack's tokens in the
|
|
270
|
+
* vault on average, which is what lets a pool survive a run of top-tier wins.
|
|
271
|
+
*/
|
|
272
|
+
function averageMultiplierBps(tiers) {
|
|
273
|
+
let weighted = 0n;
|
|
274
|
+
for (const tier of tiers) {
|
|
275
|
+
if (tier.tickets === 0) continue;
|
|
276
|
+
weighted += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
277
|
+
}
|
|
278
|
+
return Number(weighted / BigInt(TICKETS));
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/pdas.ts
|
|
282
|
+
/**
|
|
283
|
+
* Every program-derived address a client needs.
|
|
284
|
+
*
|
|
285
|
+
* The three gabox PDAs come from the generated tree, which Codama built from the IDL's own seed
|
|
286
|
+
* metadata. Re-exported here so callers have one import, and so the seeds stay in exactly one
|
|
287
|
+
* place. The two written by hand are the ones Codama cannot generate: an ATA, and a PDA that lives
|
|
288
|
+
* under a program other than ours.
|
|
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.
|
|
324
|
+
*/
|
|
325
|
+
async function scopedVrfIdentityAddress() {
|
|
326
|
+
return await getProgramDerivedAddress({
|
|
327
|
+
programAddress: VRF_PROGRAM_ADDRESS,
|
|
328
|
+
seeds: [getBytesEncoder().encode(IDENTITY_SEED), getAddressEncoder().encode(GABOX_PROGRAM_ID)]
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* An associated token account. The seed order is `[owner, token_program, mint]`, which is the ATA
|
|
333
|
+
* program's own order and not the argument order most callers remember.
|
|
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
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#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
|
+
*/
|
|
379
|
+
const DISCRIMINATOR = 8;
|
|
380
|
+
/** `Draw.pool` sits straight after the discriminator. */
|
|
381
|
+
const DRAW_POOL_OFFSET = DISCRIMINATOR;
|
|
382
|
+
/** `Draw.purchaser` sits after the discriminator and `pool`. */
|
|
383
|
+
const DRAW_PURCHASER_OFFSET = 40;
|
|
384
|
+
/** `Pool.creator` sits straight after the discriminator. */
|
|
385
|
+
const POOL_CREATOR_OFFSET = DISCRIMINATOR;
|
|
386
|
+
/** `Pool.mint` sits after the discriminator and `creator`. */
|
|
387
|
+
const POOL_MINT_OFFSET = 40;
|
|
388
|
+
/** `ReferralLink.referrer` sits after the discriminator and `referee`. */
|
|
389
|
+
const REFERRAL_LINK_REFERRER_OFFSET = 40;
|
|
390
|
+
const base58 = getBase58Decoder();
|
|
391
|
+
const base64$1 = getBase64Encoder();
|
|
392
|
+
/** A `memcmp` filter wants base58, and both discriminators and addresses arrive as other things. */
|
|
393
|
+
const asBase58 = (bytes) => base58.decode(bytes);
|
|
394
|
+
const memcmp = (offset, bytes) => ({ memcmp: {
|
|
395
|
+
offset: BigInt(offset),
|
|
396
|
+
bytes,
|
|
397
|
+
encoding: "base58"
|
|
398
|
+
} });
|
|
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
|
+
const encoded = (address, data) => ({
|
|
411
|
+
address,
|
|
412
|
+
data,
|
|
413
|
+
executable: false,
|
|
414
|
+
lamports: 0n,
|
|
415
|
+
programAddress: GABOX_PROGRAM_ID,
|
|
416
|
+
space: BigInt(data.length)
|
|
417
|
+
});
|
|
418
|
+
/** The pool for a coin, or `null` when the coin has no machine. */
|
|
419
|
+
async function fetchPoolByMint(client, mint) {
|
|
420
|
+
return await fetchPoolAt(client, await poolAddress(mint));
|
|
421
|
+
}
|
|
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
|
+
async function fetchPoolAt(client, address) {
|
|
430
|
+
return decodeCurrentPool(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
431
|
+
}
|
|
432
|
+
/** `null` for a missing pool; an error for one on the older devnet layout. */
|
|
433
|
+
function decodeCurrentPool(account) {
|
|
434
|
+
if (!account.exists) return null;
|
|
435
|
+
if (account.data.length !== getPoolSize()) throw new Error(`Machine ${account.address} uses an older devnet account layout and needs migration or recreation.`);
|
|
436
|
+
return decodePool(account).data;
|
|
437
|
+
}
|
|
438
|
+
/** A draw by address, or `null`. Delivered and legacy-claimed draws are closed. */
|
|
439
|
+
async function fetchDraw(client, address) {
|
|
440
|
+
const account = await fetchMaybeDraw(client.rpc, address, { commitment: "confirmed" });
|
|
441
|
+
return account.exists ? account.data : null;
|
|
442
|
+
}
|
|
443
|
+
/** Accrued referral reward for one referrer and pool, or zero when no referred pack has settled. */
|
|
444
|
+
async function fetchReferralReward(client, pool, referrer) {
|
|
445
|
+
const address = await referralAddress(pool, referrer);
|
|
446
|
+
const account = await fetchMaybeReferral(client.rpc, address, { commitment: "confirmed" });
|
|
447
|
+
return account.exists ? account.data : null;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Every current-layout machine, by discriminator and account size. Older devnet
|
|
451
|
+
* pools predate seed fields and must not be decoded with the current schema.
|
|
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.
|
|
456
|
+
*/
|
|
457
|
+
async function listPools(client) {
|
|
458
|
+
return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], ({ address, data }) => ({
|
|
459
|
+
address,
|
|
460
|
+
data: decodePool(encoded(address, data)).data
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
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
|
+
async function listDraws(client, query = {}) {
|
|
484
|
+
const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR))];
|
|
485
|
+
if (query.pool) filters.push(memcmp(8, query.pool));
|
|
486
|
+
if (query.purchaser) filters.push(memcmp(40, query.purchaser));
|
|
487
|
+
return await scan(client.rpc, filters, ({ address, data }) => ({
|
|
488
|
+
address,
|
|
489
|
+
data: decodeDraw(encoded(address, data)).data
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Every wallet bound to one referrer. This is the "who did I refer" query.
|
|
494
|
+
*
|
|
495
|
+
* A scan, because a link is seeded on the referee and nothing on chain indexes it by referrer.
|
|
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);
|
|
511
|
+
const { value } = await client.rpc.getAccountInfo(vault, {
|
|
512
|
+
encoding: "base64",
|
|
513
|
+
commitment: "confirmed"
|
|
514
|
+
}).send();
|
|
515
|
+
if (!value) return 0n;
|
|
516
|
+
return tokenAccountAmount(new Uint8Array(base64$1.encode(value.data[0])));
|
|
517
|
+
}
|
|
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
|
+
async function fetchPoolInventory(client, mint) {
|
|
530
|
+
const address = await poolAddress(mint);
|
|
531
|
+
const readPair = async (vault) => {
|
|
532
|
+
const [poolAccount, vaultAccount] = await fetchEncodedAccounts(client.rpc, [address, vault], { commitment: "confirmed" });
|
|
533
|
+
const pool = decodeCurrentPool(poolAccount);
|
|
534
|
+
if (!pool) return null;
|
|
535
|
+
return {
|
|
536
|
+
pool,
|
|
537
|
+
inventory: vaultAccount.exists ? tokenAccountAmount(vaultAccount.data) : 0n
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
const expectedVault = await vaultAddress(mint);
|
|
541
|
+
let pair = await readPair(expectedVault);
|
|
542
|
+
if (pair && pair.pool.vault !== expectedVault) pair = await readPair(pair.pool.vault);
|
|
543
|
+
if (!pair) return null;
|
|
544
|
+
const { pool, inventory } = pair;
|
|
545
|
+
const reserved = pool.reserved;
|
|
546
|
+
return {
|
|
547
|
+
pool,
|
|
548
|
+
poolAddress: address,
|
|
549
|
+
vault: pool.vault,
|
|
550
|
+
inventory,
|
|
551
|
+
reserved,
|
|
552
|
+
free: inventory >= reserved ? inventory - reserved : 0n
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/** The generated `Tier` uses the same field names as `math.ts`, so this is only a widening. */
|
|
556
|
+
const tiersOf = (pool) => pool.tiers.map((t) => ({
|
|
557
|
+
multiplierBps: t.multiplierBps,
|
|
558
|
+
tickets: t.tickets
|
|
559
|
+
}));
|
|
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
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/events.ts
|
|
652
|
+
/**
|
|
653
|
+
* Events, and waiting for a draw to resolve.
|
|
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. */
|
|
681
|
+
function decodeEvent(data) {
|
|
682
|
+
if (startsWith(data, POOL_CREATED_EVENT_DISCRIMINATOR)) return {
|
|
683
|
+
name: "PoolCreated",
|
|
684
|
+
data: getPoolCreatedEventDecoder().decode(data)
|
|
685
|
+
};
|
|
686
|
+
if (startsWith(data, PRIZES_FUNDED_EVENT_DISCRIMINATOR)) return {
|
|
687
|
+
name: "PrizesFunded",
|
|
688
|
+
data: getPrizesFundedEventDecoder().decode(data)
|
|
689
|
+
};
|
|
690
|
+
if (startsWith(data, PACK_BOUGHT_EVENT_DISCRIMINATOR)) return {
|
|
691
|
+
name: "PackBought",
|
|
692
|
+
data: getPackBoughtEventDecoder().decode(data)
|
|
693
|
+
};
|
|
694
|
+
if (startsWith(data, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR)) return {
|
|
695
|
+
name: "RandomnessRetried",
|
|
696
|
+
data: getRandomnessRetriedEventDecoder().decode(data)
|
|
697
|
+
};
|
|
698
|
+
if (startsWith(data, DRAW_RESOLVED_EVENT_DISCRIMINATOR)) return {
|
|
699
|
+
name: "DrawResolved",
|
|
700
|
+
data: getDrawResolvedEventDecoder().decode(data)
|
|
701
|
+
};
|
|
702
|
+
if (startsWith(data, PRIZE_REDEEMED_EVENT_DISCRIMINATOR)) return {
|
|
703
|
+
name: "PrizeRedeemed",
|
|
704
|
+
data: getPrizeRedeemedEventDecoder().decode(data)
|
|
705
|
+
};
|
|
706
|
+
if (startsWith(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
|
|
707
|
+
name: "TokensSold",
|
|
708
|
+
data: getTokensSoldEventDecoder().decode(data)
|
|
709
|
+
};
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Every gabox event in a transaction's logs, in order.
|
|
714
|
+
*
|
|
715
|
+
* A log line that is not `Program data:`, or whose payload matches no discriminator, is skipped
|
|
716
|
+
* rather than reported. Another program in the same transaction emits its own events, and they are
|
|
717
|
+
* not an error here.
|
|
718
|
+
*/
|
|
719
|
+
function decodeEvents(logs) {
|
|
720
|
+
const events = [];
|
|
721
|
+
for (const line of logs) {
|
|
722
|
+
if (!line.startsWith(PROGRAM_DATA)) continue;
|
|
723
|
+
const payload = line.slice(14).trim();
|
|
724
|
+
let bytes;
|
|
725
|
+
try {
|
|
726
|
+
bytes = new Uint8Array(base64.encode(payload));
|
|
727
|
+
} catch {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const event = decodeEvent(bytes);
|
|
731
|
+
if (event) events.push(event);
|
|
732
|
+
}
|
|
733
|
+
return events;
|
|
734
|
+
}
|
|
735
|
+
/** Fetch a transaction and decode the gabox events it emitted. */
|
|
736
|
+
async function fetchEvents(client, signature) {
|
|
737
|
+
return await readEvents(client.rpc, signature);
|
|
738
|
+
}
|
|
739
|
+
/** The RPC half of `fetchEvents`, for the internal readers that already hold one. */
|
|
740
|
+
async function readEvents(rpc, signature) {
|
|
741
|
+
return decodeEvents((await rpc.getTransaction(signature, {
|
|
742
|
+
commitment: "confirmed",
|
|
743
|
+
encoding: "json",
|
|
744
|
+
maxSupportedTransactionVersion: 0
|
|
745
|
+
}).send())?.meta?.logMessages ?? []);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Watch one draw and resolve when its callback delivers the prize.
|
|
749
|
+
*
|
|
750
|
+
* The first notification is the account as it is now, so a draw that resolved before the call
|
|
751
|
+
* resolves the promise immediately. Automatic delivery closes the account, so the watcher rebuilds
|
|
752
|
+
* the resolved snapshot from the purchase and resolution events when it sees that closure.
|
|
753
|
+
*
|
|
754
|
+
* There is no timeout here on purpose. The program's own deadline is 216,000 slots, which is about
|
|
755
|
+
* a day, and a UI should decide its own patience rather than inherit one.
|
|
756
|
+
*/
|
|
757
|
+
async function watchDraw(client, address, options = {}) {
|
|
758
|
+
const { rpc, rpcSubscriptions } = client;
|
|
759
|
+
const controller = new AbortController();
|
|
760
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
761
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
762
|
+
try {
|
|
763
|
+
const notifications = await rpcSubscriptions.accountNotifications(address, {
|
|
764
|
+
encoding: "base64",
|
|
765
|
+
commitment: "confirmed"
|
|
766
|
+
}).subscribe({ abortSignal: controller.signal });
|
|
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`);
|
|
784
|
+
}
|
|
785
|
+
const draw = decodeDrawBytes(address, new Uint8Array(base64.encode(account.data[0])));
|
|
786
|
+
options.onChange?.(draw);
|
|
787
|
+
if (draw.status === 1) return draw;
|
|
788
|
+
}
|
|
789
|
+
throw new Error(`the subscription for draw ${address} ended before it resolved`);
|
|
790
|
+
} finally {
|
|
791
|
+
options.signal?.removeEventListener("abort", abort);
|
|
792
|
+
controller.abort();
|
|
793
|
+
}
|
|
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
|
+
};
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/lookupTables.ts
|
|
864
|
+
/**
|
|
865
|
+
* Shared devnet address lookup table, verified on 2026-09-10T13:05:37.919Z.
|
|
866
|
+
* Generated by scripts/deploy-lookup-table.ts. Existing indices are immutable;
|
|
867
|
+
* keep this table active while clients use it. Authority is the devnet deploy wallet.
|
|
868
|
+
*/
|
|
869
|
+
const DEVNET_LOOKUP_TABLE_ADDRESS = address("Cx4ri1BU2bnDXPjnJykF3nbY2u4MD5pPvzFCJtNizWFa");
|
|
870
|
+
const DEVNET_LOOKUP_TABLE_ADDRESSES = [
|
|
871
|
+
address("GaBoxR9nYcK1zeu8EvSJVHV3SrYpCFmvbh2MLgobMcUA"),
|
|
872
|
+
address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
|
|
873
|
+
address("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"),
|
|
874
|
+
address("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"),
|
|
875
|
+
address("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"),
|
|
876
|
+
address("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"),
|
|
877
|
+
address("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"),
|
|
878
|
+
address("11111111111111111111111111111111"),
|
|
879
|
+
address("MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e"),
|
|
880
|
+
address("So11111111111111111111111111111111111111112"),
|
|
881
|
+
address("Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz"),
|
|
882
|
+
address("Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh"),
|
|
883
|
+
address("SysvarS1otHashes111111111111111111111111111"),
|
|
884
|
+
address("Sysvar1nstructions1111111111111111111111111"),
|
|
885
|
+
address("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"),
|
|
886
|
+
address("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"),
|
|
887
|
+
address("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt"),
|
|
888
|
+
address("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y"),
|
|
889
|
+
address("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"),
|
|
890
|
+
address("13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ"),
|
|
891
|
+
address("BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s"),
|
|
892
|
+
address("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"),
|
|
893
|
+
address("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"),
|
|
894
|
+
address("5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx"),
|
|
895
|
+
address("C2aFPdENg4A2HQsmrd5rTw5TaYBX5Ku887cWjbFKtZpw"),
|
|
896
|
+
address("7ahYg76P8bhifT1Uj3hNHGKRFPp6bLkx1ppNrWbnsfu2"),
|
|
897
|
+
address("68yFSZxzLWJXkxxRGydZ63C6mHx1NLEDWmwN9Lb5yySg"),
|
|
898
|
+
address("DLP9ADYpdQV4Z4UQDZof7iLHu2qqdzmMPjcAHDGe4jTt"),
|
|
899
|
+
address("6QgPshH1egekJ2TURfakiiApDdv98qfRuRe7RectX8xs"),
|
|
900
|
+
address("FmFPTNDmmVDhqzaqZYhnt4fj5fJP3pffcMWf2b5JnRTk"),
|
|
901
|
+
address("78i5hpHxbtmosSJdfJ74WzwdUr3eKWg9RbCPpBeAF78t"),
|
|
902
|
+
address("7611SPS3UkjsA43auxPpJpPVAkgEHg4dTVorK839GonW"),
|
|
903
|
+
address("8RMFYhsVsfdGCuWPFLxMCbSpSesiofabDdNorGqFrBNe"),
|
|
904
|
+
address("9GbQXDFHKLdr4BzZ8Cx2pkX2aM2Kg7yEeYnUCKjZGE4M"),
|
|
905
|
+
address("9GDepfBcjJMvNgmijXWVWa97Am7VZYCqXx7kJV44E9ij"),
|
|
906
|
+
address("3fyMEgHADGRrBnCVLU7u9AwpiMtmGDWViJzDQC8kgRa5"),
|
|
907
|
+
address("9ppkS5madL2uXozoEnMnZi5bKDq9jgdKkSavjWTS5NfW"),
|
|
908
|
+
address("C3PvwRFdKT6caSLxnwy8h67KWNevoboNDg6bwJZYzWB5"),
|
|
909
|
+
address("DDMCfwbcaNYTeMk1ca8tr8BQKFaUfFCWFwBJq8JcnyCw"),
|
|
910
|
+
address("FrYoobDtL7w1HrTjHAc8Ya7qQzEdJPhGXXFKskCDaA3p"),
|
|
911
|
+
address("DRDBsRMst21CJUhwD16pncgiXnBrFaRAPvA2G6SUQceE"),
|
|
912
|
+
address("J7JbDVnGKus2M9PKzH7ZbeCYugEYDgpGBfqKL85dQbU7"),
|
|
913
|
+
address("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
|
914
|
+
address("HjQjngTDqoHE6aaGhUqfz9aQ7WZcBRjy5xB8PScLSr8i"),
|
|
915
|
+
address("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
|
916
|
+
address("GAFuhgcd328SkkBYHpfadzmef9hTGAFRCi9QoCnsZQug"),
|
|
917
|
+
address("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
|
918
|
+
address("AktftA98kSWAxn6kVSoqBXBELUArjKu2H9WmKB48ULFY"),
|
|
919
|
+
address("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
|
920
|
+
address("6rVkF4HSgy1jrnC3HogfRgPHrq4CtLg5f11URpsC4i9D"),
|
|
921
|
+
address("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
|
922
|
+
address("GYH1Gae1wJytMSvMvw8JVcv7nuAbxi8i9erNVbERnzXd"),
|
|
923
|
+
address("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
|
924
|
+
address("CA7v8gHfbquYXyDnDx6QxWW8hmL1H7X6Y2RYDrGLnuck"),
|
|
925
|
+
address("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
|
926
|
+
address("CASRL2zkwDnppxEFQ4LgdwgR9pdz5Q8R8nEMKVZ9QoLp"),
|
|
927
|
+
address("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
|
928
|
+
address("qkYdTGRPHbWTWuBMz45bCiU6a23axRqf6sBHm9295WY"),
|
|
929
|
+
address("12e2F4DKkD3Lff6WPYsU7Xd76SHPEyN9T8XSsTJNF8oT"),
|
|
930
|
+
address("GjJkcak9e4L2HsxSZqVsc81L7coChdR7F3ciJYnQcSnU"),
|
|
931
|
+
address("2Ej38XSkmpvXzoUg5ZLma7Y9rCiZVgxzTdvE3Kph5juM"),
|
|
932
|
+
address("2daQRytJgLzLLziPNQBNJ7w1Ltz3XqZG4dZxBamLAf7v"),
|
|
933
|
+
address("3PAxmkxnM2vHno9amWQCsaaFjYnPGcD87HZGx1ChVjPj"),
|
|
934
|
+
address("BWS634asUFdrpYpfofFA1CrGB9wEbh9gt8XswZ4AWz9J"),
|
|
935
|
+
address("4QZqaBNm2F7viBDhhs8AQ5wC9FshgLJEiLLFGoxZZrTn"),
|
|
936
|
+
address("4JaPhJE7WgQZ3xFbxn2spU97reA13SiM99wD3RF4Lqro"),
|
|
937
|
+
address("9xvDPD6G7NRCEu7W2M9vCLeo8we23Ww7pzQEhXcuJAmA"),
|
|
938
|
+
address("AHEgRGXFn8JbhXccWM4i1meRGPFbx8kzb9BGN6ocqRFL"),
|
|
939
|
+
address("CdkG7sp1LT9YLsDaTWREaQcX6W4gZySk3o1eSjoL2uTh"),
|
|
940
|
+
address("2pLUmsYktT7gR6P5hXs9Ldo6Vg1oQB2Q4NPbJqHUjZhq"),
|
|
941
|
+
address("Freijj9xKLefjrb5fHgT6KMbYG1XBP2mA83tqeXYUMYM"),
|
|
942
|
+
address("4uzPz9TPskXiiEZ6X78rqud8LvfdxkJBr5EKHgbx4azP"),
|
|
943
|
+
address("Hxzab4UjjVH2KjsdAqzdxGdYUpNN5FKhpu7iikB869uH"),
|
|
944
|
+
address("Frkwunr9dQM9d4TthfQ2unxC994XpkLMpkRP2e7yfirk")
|
|
945
|
+
];
|
|
946
|
+
const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET_LOOKUP_TABLE_ADDRESSES] };
|
|
947
|
+
/**
|
|
948
|
+
* The tables a client compresses with when its config names none.
|
|
949
|
+
*
|
|
950
|
+
* Only devnet has a shared table today. Mainnet gets one when the program is deployed there; until
|
|
951
|
+
* then a mainnet or localnet client compresses with nothing, and a message over 1,232 bytes fails
|
|
952
|
+
* in `buildMessage` with a request for tables. Pass `addressLookupTables` to `createClient` to
|
|
953
|
+
* supply your own.
|
|
954
|
+
*/
|
|
955
|
+
function defaultAddressLookupTables(cluster) {
|
|
956
|
+
return cluster === "devnet" ? { ...DEVNET_ADDRESS_LOOKUP_TABLES } : {};
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/offer.ts
|
|
960
|
+
/**
|
|
961
|
+
* The full offer for one machine. Two round trips: the pool and its vault, then the venue.
|
|
962
|
+
*
|
|
963
|
+
* Throws when the coin has no pool.
|
|
964
|
+
*/
|
|
965
|
+
async function getOffer(client, mint, options = {}) {
|
|
966
|
+
const inventory = await fetchPoolInventory(client, mint);
|
|
967
|
+
if (!inventory) throw new Error(`no gabox pool for mint ${mint}`);
|
|
968
|
+
const venue = await resolveVenue(client, {
|
|
969
|
+
mint,
|
|
970
|
+
user: options.user ?? inventory.pool.creator,
|
|
971
|
+
...options.venue ? { venue: options.venue } : {}
|
|
972
|
+
});
|
|
973
|
+
return offerFromState(inventory, venue.kind, venue.quoteBuy(inventory.pool.packTokens));
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* 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. `quoteLamports` is
|
|
978
|
+
* `venue.quoteBuy(pool.packTokens)`.
|
|
979
|
+
*/
|
|
980
|
+
function offerFromState(inventory, venue, quoteLamports) {
|
|
981
|
+
const { pool } = inventory;
|
|
982
|
+
const tiers = tiersOf(pool);
|
|
983
|
+
const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
|
|
984
|
+
const uncapped = uncappedMaximum(pool.packTokens, tiers);
|
|
985
|
+
return {
|
|
986
|
+
mint: pool.mint,
|
|
987
|
+
pool: inventory.poolAddress,
|
|
988
|
+
packTokens: pool.packTokens,
|
|
989
|
+
quoteLamports,
|
|
990
|
+
feeBps: pool.feeBps,
|
|
991
|
+
feeLamports: share(quoteLamports, BigInt(pool.feeBps)),
|
|
992
|
+
protocolLamports: share(quoteLamports, PROTOCOL_FEE_BPS),
|
|
993
|
+
seedLamports: pool.seedLamports,
|
|
994
|
+
seedTokens: pool.seedTokens,
|
|
995
|
+
venue,
|
|
996
|
+
prizes: offer.prizes,
|
|
997
|
+
maximum: offer.maximum,
|
|
998
|
+
minimum: offer.minimum,
|
|
999
|
+
uncapped,
|
|
1000
|
+
inventory: inventory.inventory,
|
|
1001
|
+
reserved: inventory.reserved,
|
|
1002
|
+
free: inventory.free,
|
|
1003
|
+
isFirstPack: pool.nextSeq === 0n,
|
|
1004
|
+
isSeeded: offer.maximum === uncapped,
|
|
1005
|
+
maxMultiplierBps: maxMultiplierBps(tiers),
|
|
1006
|
+
averageMultiplierBps: averageMultiplierBps(tiers)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* How short of the top prize a pool is, in tokens. `0` when it pays the whole table.
|
|
1011
|
+
*
|
|
1012
|
+
* The pack brings its own `packTokens` into the vault before the offer is computed, so the vault
|
|
1013
|
+
* only has to hold `uncapped - packTokens` beforehand. Anything already reserved by another draw
|
|
1014
|
+
* does not count. A donation of this size through `fund_prizes` uncaps the top prize again.
|
|
1015
|
+
*/
|
|
1016
|
+
function seedShortfall(offer) {
|
|
1017
|
+
const needed = offer.uncapped > offer.packTokens ? offer.uncapped - offer.packTokens : 0n;
|
|
1018
|
+
return offer.free >= needed ? 0n : needed - offer.free;
|
|
1019
|
+
}
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region src/rpc.ts
|
|
1022
|
+
/**
|
|
1023
|
+
* The client, and the cluster guard.
|
|
1024
|
+
*
|
|
1025
|
+
* `createClient` is the SDK's init step. It takes the cluster and the RPC endpoint once and returns
|
|
1026
|
+
* one object that every other chain-touching function in this SDK takes as its first argument: the
|
|
1027
|
+
* RPC, the subscriptions client, and the address lookup tables that cluster compresses with.
|
|
1028
|
+
*
|
|
1029
|
+
* # Why `cluster` has no default
|
|
1030
|
+
*
|
|
1031
|
+
* v1's simulator was one empty wallet away from running against mainnet. Nothing in the code said
|
|
1032
|
+
* which cluster it was pointed at; the answer lived in a shell variable and in the operator's head.
|
|
1033
|
+
* The failure would not have been a crash. It would have been real transactions on real money,
|
|
1034
|
+
* discovered afterwards.
|
|
1035
|
+
*
|
|
1036
|
+
* So the cluster is a property of the code, not of the environment. The caller names it in the
|
|
1037
|
+
* same call that names the URL, and the two are checked against each other:
|
|
1038
|
+
*
|
|
1039
|
+
* - `devnet` needs a URL that names devnet. A URL that names nothing is refused too, because
|
|
1040
|
+
* "I thought this was devnet" is exactly the accident this guard exists for.
|
|
1041
|
+
* - `mainnet-beta` and `localnet` refuse a URL that names a different cluster. A URL that names
|
|
1042
|
+
* nothing is allowed: private mainnet endpoints often do not say "mainnet", and a local
|
|
1043
|
+
* validator never says anything.
|
|
1044
|
+
*
|
|
1045
|
+
* Mainnet is one word away. It is a word the caller has to write.
|
|
1046
|
+
*/
|
|
1047
|
+
/** Solana's public endpoints, and the test validator's default ports. */
|
|
1048
|
+
const CLUSTER_ENDPOINTS = {
|
|
1049
|
+
devnet: {
|
|
1050
|
+
url: "https://api.devnet.solana.com",
|
|
1051
|
+
wsUrl: "wss://api.devnet.solana.com"
|
|
1052
|
+
},
|
|
1053
|
+
"mainnet-beta": {
|
|
1054
|
+
url: "https://api.mainnet-beta.solana.com",
|
|
1055
|
+
wsUrl: "wss://api.mainnet-beta.solana.com"
|
|
1056
|
+
},
|
|
1057
|
+
localnet: {
|
|
1058
|
+
url: "http://127.0.0.1:8899",
|
|
1059
|
+
wsUrl: "ws://127.0.0.1:8900"
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const DEVNET_HTTP = CLUSTER_ENDPOINTS.devnet.url;
|
|
1063
|
+
const DEVNET_WS = CLUSTER_ENDPOINTS.devnet.wsUrl;
|
|
1064
|
+
/**
|
|
1065
|
+
* The cluster a URL names, from its text alone. A substring check, deliberately: providers spell
|
|
1066
|
+
* it many ways. `null` when the URL names none, which is a local validator or a private endpoint.
|
|
1067
|
+
*/
|
|
1068
|
+
function clusterNamedBy(url) {
|
|
1069
|
+
const lower = url.toLowerCase();
|
|
1070
|
+
if (lower.includes("devnet")) return "devnet";
|
|
1071
|
+
if (lower.includes("mainnet")) return "mainnet-beta";
|
|
1072
|
+
if (lower.includes("testnet")) return "testnet";
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Refuse a URL that contradicts the declared cluster. Exported so a script can check a URL before
|
|
1077
|
+
* it does anything else with it. The rules are in the file comment.
|
|
1078
|
+
*/
|
|
1079
|
+
function assertClusterUrl(cluster, url) {
|
|
1080
|
+
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.\nEvery address in this SDK — the Pump programs, the MagicBlock queue, the pools — exists on every cluster, so a wrong URL is a live transaction, not an error. Pass { cluster: 'localnet' } for a local validator, or name the cluster the URL really is.`);
|
|
1082
|
+
if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\nEvery address in this SDK exists on every cluster, so a wrong URL is a live transaction, not an error. Pass the cluster the URL really names.`);
|
|
1083
|
+
}
|
|
1084
|
+
/** `https://x` becomes `wss://x`, `http://x` becomes `ws://x`. Anything else is returned as is. */
|
|
1085
|
+
function websocketUrlFor(url) {
|
|
1086
|
+
if (url.startsWith("https://")) return `wss://${url.slice(8)}`;
|
|
1087
|
+
if (url.startsWith("http://")) return `ws://${url.slice(7)}`;
|
|
1088
|
+
return url;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* The SDK's init step. Call it once and pass the result everywhere.
|
|
1092
|
+
*
|
|
1093
|
+
* Both RPC clients are created together because everything in this SDK that watches a draw needs
|
|
1094
|
+
* the pair: the subscription reports the change, and the RPC reads the account that changed.
|
|
1095
|
+
*/
|
|
1096
|
+
function createClient(config) {
|
|
1097
|
+
const { cluster } = config;
|
|
1098
|
+
const defaults = CLUSTER_ENDPOINTS[cluster];
|
|
1099
|
+
if (!defaults) throw new Error(`unknown cluster ${JSON.stringify(cluster)}; expected 'devnet', 'mainnet-beta' or 'localnet'`);
|
|
1100
|
+
const url = config.url ?? defaults.url;
|
|
1101
|
+
assertClusterUrl(cluster, url);
|
|
1102
|
+
const wsUrl = config.wsUrl ?? (config.url === void 0 ? defaults.wsUrl : websocketUrlFor(url));
|
|
1103
|
+
assertClusterUrl(cluster, wsUrl);
|
|
1104
|
+
return {
|
|
1105
|
+
cluster,
|
|
1106
|
+
url,
|
|
1107
|
+
wsUrl,
|
|
1108
|
+
rpc: createSolanaRpc(url),
|
|
1109
|
+
rpcSubscriptions: createSolanaRpcSubscriptions(wsUrl),
|
|
1110
|
+
addressLookupTables: config.addressLookupTables ?? defaultAddressLookupTables(cluster)
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/tx/message.ts
|
|
1115
|
+
/**
|
|
1116
|
+
* Assembling a transaction message.
|
|
1117
|
+
*
|
|
1118
|
+
* Every builder in this directory ends here: compute budget first, then the program instructions,
|
|
1119
|
+
* with a fee payer and a blockhash lifetime. The result is a message a wallet can sign and send —
|
|
1120
|
+
* nothing in this SDK signs or sends anything itself.
|
|
1121
|
+
*/
|
|
1122
|
+
/**
|
|
1123
|
+
* Build the message. One RPC read, for the blockhash.
|
|
1124
|
+
*
|
|
1125
|
+
* The blockhash expires in about a minute, so build the message when the user is ready to sign
|
|
1126
|
+
* rather than when the page loads.
|
|
1127
|
+
*/
|
|
1128
|
+
async function buildMessage(client, feePayer, instructions, options) {
|
|
1129
|
+
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
|
|
1130
|
+
const budget = computeBudgetInstructions(options.computeUnitLimit, options.computeUnitPrice);
|
|
1131
|
+
const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(feePayer, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions([...budget, ...instructions], m), (m) => compressTransactionMessageUsingAddressLookupTables(m, options.addressLookupTables ?? client.addressLookupTables));
|
|
1132
|
+
const size = getTransactionEncoder().encode(compileTransaction(message)).length;
|
|
1133
|
+
if (size > 1232) throw new Error(`Transaction is ${size} bytes; Solana allows 1232. Shorten metadata or supply additional address lookup tables.`);
|
|
1134
|
+
return message;
|
|
1135
|
+
}
|
|
1136
|
+
/** Append `remainingAccounts` to a generated instruction, which is how a venue's list is passed. */
|
|
1137
|
+
function withRemainingAccounts(instruction, remaining) {
|
|
1138
|
+
return {
|
|
1139
|
+
...instruction,
|
|
1140
|
+
accounts: [...instruction.accounts ?? [], ...remaining]
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
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;
|
|
1151
|
+
return {
|
|
1152
|
+
link,
|
|
1153
|
+
referrer,
|
|
1154
|
+
referral: await referralAddress(pool, referrer)
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/** Bind a wallet to a referrer. The purchaser signs and the binding is permanent. */
|
|
1158
|
+
async function bindReferrer(client, referee, referrer, options = {}) {
|
|
1159
|
+
return await buildMessage(client, referee, [await getBindReferrerInstructionAsync({
|
|
1160
|
+
referee,
|
|
1161
|
+
referrer
|
|
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 }
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#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
|
+
async function buyPack(client, input) {
|
|
1212
|
+
const { mint, purchaser } = input;
|
|
1213
|
+
if (input.maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
1214
|
+
const inventory = await fetchPoolInventory(client, mint);
|
|
1215
|
+
if (!inventory) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1216
|
+
const { pool, poolAddress } = inventory;
|
|
1217
|
+
const venue = await resolveVenue(client, {
|
|
1218
|
+
mint,
|
|
1219
|
+
user: purchaser.address,
|
|
1220
|
+
...input.venue ? { venue: input.venue } : {}
|
|
1221
|
+
});
|
|
1222
|
+
const draw = await drawAddress(poolAddress, input.seq ?? pool.nextSeq);
|
|
1223
|
+
let referral = await resolveReferral(client, purchaser.address, poolAddress);
|
|
1224
|
+
let bind = null;
|
|
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,
|
|
1239
|
+
pool: poolAddress,
|
|
1240
|
+
creator: pool.creator,
|
|
1241
|
+
draw,
|
|
1242
|
+
mint,
|
|
1243
|
+
vault: pool.vault,
|
|
1244
|
+
userTokens: venueUserTokens(venue.buyAccounts, venue.kind),
|
|
1245
|
+
venue: venue.program,
|
|
1246
|
+
identity: await vrfIdentityAddress(),
|
|
1247
|
+
tokenProgram: pool.tokenProgram,
|
|
1248
|
+
maxQuoteIn: input.maxQuoteIn,
|
|
1249
|
+
minMaximum: input.minMaximum,
|
|
1250
|
+
maxTotalDebit: input.maxTotalDebit,
|
|
1251
|
+
referralLink: referral?.link,
|
|
1252
|
+
referral: referral?.referral
|
|
1253
|
+
});
|
|
1254
|
+
const remaining = input.cashback ? [...venue.buyAccounts, swapCashbackAccount(purchaser.address)] : venue.buyAccounts;
|
|
1255
|
+
return await buildMessage(client, purchaser, [
|
|
1256
|
+
...bind === null ? [] : [bind],
|
|
1257
|
+
...venue.kind === "pumpswap" ? wsolPreparation$2(purchaser, input.wrapLamports ?? input.maxQuoteIn) : [],
|
|
1258
|
+
withRemainingAccounts(buy, remaining)
|
|
1259
|
+
], {
|
|
1260
|
+
addressLookupTables: input.addressLookupTables,
|
|
1261
|
+
computeUnitLimit: input.computeUnitLimit ?? 5e5 + (bind === null ? 0 : 5e4),
|
|
1262
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
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
|
+
//#endregion
|
|
1312
|
+
//#region src/tx/createMachine.ts
|
|
1313
|
+
/**
|
|
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
|
+
* Build the transaction message. Sign it with both `creator` and `mintKeypair`.
|
|
1344
|
+
*
|
|
1345
|
+
* Reads Pump's `Global` account, because two of the buy accounts — the fee recipient and the
|
|
1346
|
+
* buyback fee recipient — are chosen from lists held there. Nothing else needs the chain: the coin
|
|
1347
|
+
* does not exist yet, so every other account is a derivation.
|
|
1348
|
+
*/
|
|
1349
|
+
async function createMachine(client, input) {
|
|
1350
|
+
const { creator, mintKeypair, name, symbol, uri, feeBps, riskProfile, jackpotBps, maxSeedLamports } = input;
|
|
1351
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 100) throw new Error(`feeBps must be a whole number from 0 to 100`);
|
|
1352
|
+
const tiers = jackpotTiers(jackpotBps, riskProfile);
|
|
1353
|
+
if (seedTokens(PACK_TOKENS, tiers) > 0n && maxSeedLamports <= 0n) throw new Error("maxSeedLamports must be positive when the jackpot needs a seed");
|
|
1354
|
+
const mint = mintKeypair.address;
|
|
1355
|
+
const create = getPumpCreateV2Instruction({
|
|
1356
|
+
mint: mintKeypair,
|
|
1357
|
+
user: creator,
|
|
1358
|
+
name,
|
|
1359
|
+
symbol,
|
|
1360
|
+
uri,
|
|
1361
|
+
creator: creator.address,
|
|
1362
|
+
mayhemMode: false,
|
|
1363
|
+
cashback: false
|
|
1364
|
+
});
|
|
1365
|
+
const venueAccounts = await pumpSeedBuyAccounts(client, {
|
|
1366
|
+
mint,
|
|
1367
|
+
user: creator.address,
|
|
1368
|
+
...input.feeRecipientIndex === void 0 ? {} : { feeRecipientIndex: input.feeRecipientIndex },
|
|
1369
|
+
...input.buybackRecipientIndex === void 0 ? {} : { buybackRecipientIndex: input.buybackRecipientIndex }
|
|
1370
|
+
});
|
|
1371
|
+
return await buildMessage(client, creator, [create, withRemainingAccounts(await getInitializePoolInstructionAsync({
|
|
1372
|
+
creator,
|
|
1373
|
+
mint,
|
|
1374
|
+
tokenProgram: PUMP_CREATE_TOKEN_PROGRAM_ADDRESS,
|
|
1375
|
+
venue: PUMP_PROGRAM_ADDRESS,
|
|
1376
|
+
feeBps,
|
|
1377
|
+
tiers,
|
|
1378
|
+
maxSeedLamports
|
|
1379
|
+
}), venueAccounts)], {
|
|
1380
|
+
addressLookupTables: input.addressLookupTables,
|
|
1381
|
+
computeUnitLimit: input.computeUnitLimit ?? 6e5,
|
|
1382
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* What the seed for this jackpot costs, fees included, and how many tokens it is.
|
|
1387
|
+
*
|
|
1388
|
+
* The coin does not exist yet, so the price is Pump's default new curve. Nothing else trades on
|
|
1389
|
+
* it before `initialize_pool` runs in the same transaction, so this is exact up to a change in
|
|
1390
|
+
* Pump's fee settings between the read and the send. Add a small margin for `maxSeedLamports`.
|
|
1391
|
+
*/
|
|
1392
|
+
async function seedCostEstimate(client, jackpotBps, riskProfile) {
|
|
1393
|
+
const tiers = jackpotTiers(jackpotBps, riskProfile);
|
|
1394
|
+
const seed = seedTokens(PACK_TOKENS, tiers);
|
|
1395
|
+
return {
|
|
1396
|
+
tiers,
|
|
1397
|
+
seedTokens: seed,
|
|
1398
|
+
lamports: seed === 0n ? 0n : await newCurveBuyCost(client, seed)
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* The Pump buy accounts for the seed, built without reading the bonding curve.
|
|
1403
|
+
*
|
|
1404
|
+
* The curve does not exist yet — `create_v2` in the same transaction is what creates it — so
|
|
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`.
|
|
1408
|
+
*/
|
|
1409
|
+
async function pumpSeedBuyAccounts(client, options) {
|
|
1410
|
+
const { value } = await client.rpc.getAccountInfo(PUMP_GLOBAL, {
|
|
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
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const randomIndex = (length) => {
|
|
1431
|
+
if (length === 0) throw new Error("the list is empty");
|
|
1432
|
+
return Math.floor(Math.random() * length);
|
|
1433
|
+
};
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/tx/draw.ts
|
|
1436
|
+
/** A retry and an expiry are both small. The default budget is plenty. */
|
|
1437
|
+
const DRAW_COMPUTE_UNITS = 2e5;
|
|
1438
|
+
async function retryDraw(client, input) {
|
|
1439
|
+
const retry = getRetryDrawInstruction({
|
|
1440
|
+
payer: input.payer,
|
|
1441
|
+
pool: input.pool,
|
|
1442
|
+
draw: input.draw,
|
|
1443
|
+
identity: await vrfIdentityAddress(),
|
|
1444
|
+
maxVrfDebit: input.maxVrfDebit
|
|
1445
|
+
});
|
|
1446
|
+
return await buildMessage(client, input.payer, [retry], {
|
|
1447
|
+
addressLookupTables: input.addressLookupTables,
|
|
1448
|
+
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1449
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
async function expireDraw(client, input) {
|
|
1453
|
+
const draw = await fetchDraw(client, input.draw);
|
|
1454
|
+
if (!draw) throw new Error(`no draw at ${input.draw}`);
|
|
1455
|
+
if (draw.pool !== input.pool) throw new Error(`draw ${input.draw} does not belong to ${input.pool}`);
|
|
1456
|
+
const pool = await fetchPoolAt(client, input.pool);
|
|
1457
|
+
if (!pool) throw new Error(`no pool at ${input.pool}`);
|
|
1458
|
+
const expire = getExpireDrawInstruction({
|
|
1459
|
+
pool: input.pool,
|
|
1460
|
+
draw: input.draw,
|
|
1461
|
+
purchaser: draw.purchaser,
|
|
1462
|
+
mint: pool.mint,
|
|
1463
|
+
vault: pool.vault,
|
|
1464
|
+
userTokens: await associatedTokenAddress(draw.purchaser, pool.mint, pool.tokenProgram),
|
|
1465
|
+
tokenProgram: pool.tokenProgram
|
|
1466
|
+
});
|
|
1467
|
+
return await buildMessage(client, input.payer, [expire], {
|
|
1468
|
+
addressLookupTables: input.addressLookupTables,
|
|
1469
|
+
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1470
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* What a client may do to a draw right now.
|
|
1475
|
+
*
|
|
1476
|
+
* Reads the draw and the current slot, and reproduces the program's three retry conditions and its
|
|
1477
|
+
* one expiry condition. Showing a disabled button with a countdown beats sending a transaction that
|
|
1478
|
+
* fails with `RetryTooSoon`.
|
|
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;
|
|
1487
|
+
const slotsUntilRetry = now >= retryAt ? 0n : retryAt - now;
|
|
1488
|
+
const slotsUntilExpiry = now >= expireAt ? 0n : expireAt - now;
|
|
1489
|
+
const pending = record.status === 0;
|
|
1490
|
+
return {
|
|
1491
|
+
status: record.status,
|
|
1492
|
+
attempts: record.attempts,
|
|
1493
|
+
slotsUntilRetry,
|
|
1494
|
+
slotsUntilExpiry,
|
|
1495
|
+
canRetry: pending && slotsUntilRetry === 0n && record.attempts < 3 && slotsUntilExpiry > 0n,
|
|
1496
|
+
canExpire: pending && slotsUntilExpiry === 0n
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
//#endregion
|
|
1500
|
+
//#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
|
+
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
|
+
async function fundPrizes(client, input) {
|
|
1516
|
+
const { mint, funder, amount } = input;
|
|
1517
|
+
if (amount <= 0n) throw new Error("amount must be positive");
|
|
1518
|
+
const pool = await fetchPoolByMint(client, mint);
|
|
1519
|
+
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1520
|
+
return await buildMessage(client, funder, [getFundPrizesInstruction({
|
|
1521
|
+
funder,
|
|
1522
|
+
pool: await poolAddress(mint),
|
|
1523
|
+
mint,
|
|
1524
|
+
source: input.source ?? await associatedTokenAddress(funder.address, mint, pool.tokenProgram),
|
|
1525
|
+
vault: pool.vault,
|
|
1526
|
+
tokenProgram: pool.tokenProgram,
|
|
1527
|
+
amount
|
|
1528
|
+
})], {
|
|
1529
|
+
addressLookupTables: input.addressLookupTables,
|
|
1530
|
+
computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
|
|
1531
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1532
|
+
});
|
|
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
|
+
], {
|
|
1583
|
+
addressLookupTables: input.addressLookupTables,
|
|
1584
|
+
computeUnitLimit: input.computeUnitLimit ?? FUND_WITH_BUY_COMPUTE_UNITS,
|
|
1585
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
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
|
+
//#endregion
|
|
1608
|
+
//#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
|
+
async function sellTokens(client, input) {
|
|
1628
|
+
const { mint, seller, amount, minQuoteOutput } = input;
|
|
1629
|
+
if (amount <= 0n) throw new Error("amount must be positive");
|
|
1630
|
+
if (minQuoteOutput <= 0n) throw new Error("minQuoteOutput must be positive");
|
|
1631
|
+
const pool = await fetchPoolByMint(client, mint);
|
|
1632
|
+
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1633
|
+
const venue = await resolveVenue(client, {
|
|
1634
|
+
mint,
|
|
1635
|
+
user: seller.address,
|
|
1636
|
+
...input.venue ? { venue: input.venue } : {}
|
|
1637
|
+
});
|
|
1638
|
+
const sell = getSellTokensInstruction({
|
|
1639
|
+
seller,
|
|
1640
|
+
pool: await poolAddress(mint),
|
|
1641
|
+
mint,
|
|
1642
|
+
userTokens: await associatedTokenAddress(seller.address, mint, pool.tokenProgram),
|
|
1643
|
+
venue: venue.program,
|
|
1644
|
+
feeCollectorWsol: await feeCollectorWsolAddress(),
|
|
1645
|
+
tokenProgram: pool.tokenProgram,
|
|
1646
|
+
amount,
|
|
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 } : {}
|
|
1692
|
+
});
|
|
1693
|
+
const sell = getSellPrizeInstruction({
|
|
1694
|
+
purchaser,
|
|
1695
|
+
pool: await poolAddress(mint),
|
|
1696
|
+
draw,
|
|
1697
|
+
mint,
|
|
1698
|
+
vault: pool.vault,
|
|
1699
|
+
userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
|
|
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)], {
|
|
1706
|
+
addressLookupTables: input.addressLookupTables,
|
|
1707
|
+
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1708
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
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
|
+
//#endregion
|
|
1744
|
+
//#region src/vrf.ts
|
|
1745
|
+
/**
|
|
1746
|
+
* Build the group. Nothing here reads the chain, so it is safe to call on every render.
|
|
1747
|
+
*
|
|
1748
|
+
* The generated instruction builders default `queue`, `program` and `slotHashes` on their own, so
|
|
1749
|
+
* passing this whole object is belt and braces. It is worth having anyway: a caller can show the
|
|
1750
|
+
* four accounts a draw request will touch before asking for a signature.
|
|
1751
|
+
*/
|
|
1752
|
+
async function oracleAccounts() {
|
|
1753
|
+
return {
|
|
1754
|
+
identity: await vrfIdentityAddress(),
|
|
1755
|
+
queue: VRF_DEFAULT_QUEUE,
|
|
1756
|
+
program: VRF_PROGRAM_ADDRESS,
|
|
1757
|
+
slotHashes: SLOT_HASHES_SYSVAR
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
//#endregion
|
|
1761
|
+
export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, BIND_REFERRER_COMPUTE_UNITS, BPS, BUY_PACK_COMPUTE_UNITS, CLUSTER_ENDPOINTS, COMPUTE_BUDGET_PROGRAM_ADDRESS, CREATE_MACHINE_COMPUTE_UNITS, DEFAULT_COMPUTE_UNIT_LIMIT, DEVNET_ADDRESS_LOOKUP_TABLES, DEVNET_HTTP, DEVNET_LOOKUP_TABLE_ADDRESS, DEVNET_LOOKUP_TABLE_ADDRESSES, DEVNET_WS, DRAW_DISCRIMINATOR, DRAW_POOL_OFFSET, DRAW_PURCHASER_OFFSET, GABOX_PROGRAM_ID, GaboxMathError, IDENTITY_SEED, INSTRUCTIONS_SYSVAR, MAX_ATTEMPTS, MAX_COMPUTE_UNIT_LIMIT, MAX_FEE_BPS, MAYHEM_PROGRAM_ADDRESS, PACK_TOKENS, POOL_CREATOR_OFFSET, POOL_DISCRIMINATOR, POOL_MINT_OFFSET, PROTOCOL_FEE_BPS, PROTOCOL_FEE_COLLECTOR, PUMP_FEE_PROGRAM_ADDRESS, PUMP_PROGRAM_ADDRESS, PUMP_SWAP_PROGRAM_ADDRESS, REDEEM_COMPUTE_UNITS, REFERRAL_FEE_BPS, REFERRAL_LINK_REFERRER_OFFSET, RETRY_SLOTS, SLOT_HASHES_SYSVAR, SYSTEM_PROGRAM_ADDRESS, TICKETS, TIERS, TIMEOUT_SLOTS, TOKEN_2022_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, VRF_DEFAULT_QUEUE, VRF_PROGRAM_ADDRESS, WSOL_MINT, assertClusterUrl, associatedTokenAddress, averageMultiplierBps, bindReferrer, buildMessage, buyPack, choose, claimPrize, claimReferral, clusterNamedBy, computeBudgetInstructions, createClient, createMachine, decodeDraw, decodeEvent, decodeEvents, decodePool, defaultAddressLookupTables, drawAddress, drawAvailability, expireDraw, feeCollectorWsolAddress, fetchDraw, fetchEvents, fetchPoolAt, fetchPoolByMint, fetchPoolInventory, fetchReferralReward, fetchVaultBalance, findDrawPda, findIdentityPda, findPoolPda, findReferralLinkPda, fundPrizes, fundPrizesWithBuy, generated_exports as generated, getOffer, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, jackpotTiers, listDraws, listDrawsByPool, listDrawsByPurchaser, listPools, listReferralLinksByReferrer, maxMultiplierBps, offerFor, offerFromState, oracleAccounts, poolAddress, pump_exports as pump, pumpSeedBuyAccounts, quote, quoteSellPrize, referralAddress, referralLinkAddress, resolveReferral, resolveReservation, retryDraw, scopedVrfIdentityAddress, seedCostEstimate, seedShortfall, seedTokens, sellPrize, sellTokens, share, tierAmount, tiersOf, uncappedMaximum, validatePack, validateTiers, vaultAddress, vrfIdentityAddress, watchDraw, websocketUrlFor, withRemainingAccounts };
|
|
1762
|
+
|
|
1763
|
+
//# sourceMappingURL=index.js.map
|