@gabox-labs/sdk 0.2.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +310 -0
- package/README.md +173 -438
- package/dist/{gaboxV2-CV2XltqC.js → gabox-DGTCh34U.js} +575 -1584
- package/dist/gabox-DGTCh34U.js.map +1 -0
- package/dist/generated/index.d.ts +384 -752
- package/dist/generated/index.js +116 -338
- package/dist/generated/index.js.map +1 -1
- package/dist/index-C4at2cZ_.d.ts +1184 -0
- package/dist/index.d.ts +474 -432
- package/dist/index.js +1186 -1374
- package/dist/index.js.map +1 -1
- package/dist/raydium/index.d.ts +2 -0
- package/dist/raydium/index.js +2 -0
- package/dist/raydium-CU-tZzIk.js +3282 -0
- package/dist/raydium-CU-tZzIk.js.map +1 -0
- package/llms.txt +7 -3
- package/package.json +11 -16
- package/skills/gabox-sdk/SKILL.md +57 -146
- package/skills/gabox-sdk/references/api.md +155 -145
- package/dist/gaboxV2-CV2XltqC.js.map +0 -1
- package/dist/index-BxvSkzCO.d.ts +0 -471
- package/dist/pump/index.d.ts +0 -2
- package/dist/pump/index.js +0 -2
- package/dist/pump-D0K_0uiC.js +0 -1531
- package/dist/pump-D0K_0uiC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,844 +1,141 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as
|
|
3
|
-
import { DRAW_RESOLVED_EVENT_DISCRIMINATOR, PACK_BOUGHT_EVENT_DISCRIMINATOR, POOL_CREATED_EVENT_DISCRIMINATOR, PRIZES_FUNDED_EVENT_DISCRIMINATOR,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Dt as POOL_DISCRIMINATOR, I as getFundPrizesInstruction, Ot as decodePool, Rt as DRAW_DISCRIMINATOR, W as getExpireDrawInstruction, _t as decodeWalletActivity, b as getRetryDrawInstruction, ft as findPoolPda, gt as WALLET_ACTIVITY_DISCRIMINATOR, ht as findActivityPda, k as getInitializePoolInstructionAsync, mt as findDrawPda, p as getSellTokensInstructionAsync, pt as findIdentityPda, st as getBuyPackInstructionAsync, zt as decodeDraw } from "./gabox-DGTCh34U.js";
|
|
2
|
+
import { $ as computeBudgetInstructions, $t as launchlabVaultAddress, B as fetchQuoteAsset, Bt as vaultAddress, Cn as PACK_TOKENS, Dn as VRF_DEFAULT_QUEUE, E as cpmmSwapBaseOutput, En as TIMEOUT_SLOTS, Et as decodeCpmmPool, Ft as activityAddress, G as readAccounts, H as fetchQuoteDisplay, Ht as ata, I as launchlabBuyAccounts, In as LAUNCH_DECIMALS, It as associatedTokenAddress, J as COMPUTE_BUDGET_PROGRAM_ADDRESS, K as BUY_PACK_COMPUTE_UNITS, Kn as ASSOCIATED_TOKEN_PROGRAM_ADDRESS, Kt as creatorFeeVaultAddress, Lt as drawAddress, Nt as tokenAccountAmount, O as curveBuyExactOut, On as VRF_PROGRAM_ADDRESS, Q as REDEEM_COMPUTE_UNITS, Qt as launchlabPoolAddress, Rt as poolAddress, S as wsolAccountFor, Sn as MAX_ATTEMPTS, T as cpmmSwapBaseInput, Tn as SLOT_HASHES_SYSVAR, Tt as decodeCpmmAmmConfig, Vt as vrfIdentityAddress, Wn as raydiumIds, X as DEFAULT_COMPUTE_UNIT_LIMIT, Xn as CPMM_SWAP_BASE_INPUT, Y as CREATE_MACHINE_COMPUTE_UNITS, Z as MAX_COMPUTE_UNIT_LIMIT, Zn as CPMM_SWAP_BASE_OUTPUT, _n as validatePack, _t as tiersOf, an as MAX_MULTIPLIER_BPS, at as POOL_MINT_OFFSET, b as resolveVenue, bn as IDENTITY_SEED, c as getLaunchInstruction, cn as TIERS, cr as METAPLEX_PROGRAM_ADDRESS, ct as fetchPoolByMint, dn as maxMultiplierBps, dt as fetchWalletActivity, et as getSetComputeUnitLimitInstruction, f as creatorFeeOnInput, fn as quote, ft as listDraws, gn as uncappedMaximum, gr as WSOL_MINT, gt as quotePool, hn as tierAmount, hr as TOKEN_PROGRAM_ADDRESS, ht as listPools, in as GaboxMathError, it as POOL_CREATOR_OFFSET, l as buildMessage, ln as averageMultiplierBps, lr as PLATFORM_ADMIN, lt as fetchPoolInventory, m as fetchCurveSettings, mn as seedTokens, mt as listDrawsByPurchaser, nn as BPS, nt as DRAW_POOL_OFFSET, on as MIN_SEED_MULTIPLIER_BPS, ot as fetchDraw, pn as resolveReservation, pr as SYSTEM_PROGRAM_ADDRESS, pt as listDrawsByPool, q as CLAIM_COMPUTE_UNITS, rn as DEFAULT_TIERS, rt as DRAW_PURCHASER_OFFSET, sn as TICKETS, sr as LAUNCH_TOTAL_BASE_SELL, st as fetchPoolAt, t as raydium_exports, tn as platformFeeVaultAddress, tt as getSetComputeUnitPriceInstruction, u as withRemainingAccounts, un as choose, ut as fetchVaultBalance, v as newCurveReserves, vn as validateTiers, wn as RETRY_SLOTS, xn as INSTRUCTIONS_SYSVAR, y as quoteAccountFor, yn as GABOX_PROGRAM_ID, z as order, zt as scopedVrfIdentityAddress } from "./raydium-CU-tZzIk.js";
|
|
3
|
+
import { DRAW_RESOLVED_EVENT_DISCRIMINATOR, PACK_BOUGHT_EVENT_DISCRIMINATOR, POOL_CREATED_EVENT_DISCRIMINATOR, PRIZES_FUNDED_EVENT_DISCRIMINATOR, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR, TOKENS_SOLD_EVENT_DISCRIMINATOR, getDrawResolvedEventDecoder, getPackBoughtEventDecoder, getPoolCreatedEventDecoder, getPrizesFundedEventDecoder, getRandomnessRetriedEventDecoder, getTokensSoldEventDecoder, t as generated_exports } from "./generated/index.js";
|
|
4
|
+
import { AccountRole, address, createNoopSigner, createSolanaRpc, createSolanaRpcSubscriptions, getAddressDecoder, getBase64Encoder, getU64Encoder } from "@solana/kit";
|
|
5
|
+
import { getCloseAccountInstruction, getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
|
|
6
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 = 4294967295;
|
|
40
|
-
/**
|
|
41
|
-
* The default prize table. The program does not enforce this table: it accepts any table that
|
|
42
|
-
* passes `validateTiers`. This is only the table the Gabox app creates its pools with, and the
|
|
43
|
-
* starting point for a client that has no reason to pick another one.
|
|
44
|
-
*
|
|
45
|
-
* Common 75% at 0.52x, Rare 20% at 1.2x, Epic 4% at 3x, Mythic 1% at 20x. Expected payout is
|
|
46
|
-
* 0.9499x of a pack, so the seed is 19 packs.
|
|
47
|
-
*/
|
|
48
|
-
const DEFAULT_TIERS = Object.freeze([
|
|
49
|
-
Object.freeze({
|
|
50
|
-
multiplierBps: 5201,
|
|
51
|
-
tickets: 49153
|
|
52
|
-
}),
|
|
53
|
-
Object.freeze({
|
|
54
|
-
multiplierBps: 12e3,
|
|
55
|
-
tickets: 13107
|
|
56
|
-
}),
|
|
57
|
-
Object.freeze({
|
|
58
|
-
multiplierBps: 3e4,
|
|
59
|
-
tickets: 2621
|
|
60
|
-
}),
|
|
61
|
-
Object.freeze({
|
|
62
|
-
multiplierBps: 2e5,
|
|
63
|
-
tickets: 655
|
|
64
|
-
}),
|
|
65
|
-
Object.freeze({
|
|
66
|
-
multiplierBps: 0,
|
|
67
|
-
tickets: 0
|
|
68
|
-
}),
|
|
69
|
-
Object.freeze({
|
|
70
|
-
multiplierBps: 0,
|
|
71
|
-
tickets: 0
|
|
72
|
-
}),
|
|
73
|
-
Object.freeze({
|
|
74
|
-
multiplierBps: 0,
|
|
75
|
-
tickets: 0
|
|
76
|
-
}),
|
|
77
|
-
Object.freeze({
|
|
78
|
-
multiplierBps: 0,
|
|
79
|
-
tickets: 0
|
|
80
|
-
})
|
|
81
|
-
]);
|
|
82
|
-
function checkedU64(value, what) {
|
|
83
|
-
if (value < 0n || value > U64_MAX) throw new GaboxMathError("Arithmetic", `${what} does not fit in u64`);
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
/** Tier fields encode as program `u32`s. Reject JavaScript-only values before `BigInt` or codecs. */
|
|
87
|
-
function checkedU32(value, what) {
|
|
88
|
-
if (!Number.isInteger(value) || value < 0 || value > U32_MAX) throw new GaboxMathError("InvalidDistribution", `${what} must be an integer from 0 to ${U32_MAX}`);
|
|
89
|
-
return value;
|
|
90
|
-
}
|
|
91
|
-
/** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
|
|
92
|
-
function tierAmount(base, multiplierBps) {
|
|
93
|
-
return checkedU64(base * BigInt(checkedU32(multiplierBps, "multiplierBps")) / BPS, "tier amount");
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* `math::validate`. Checks the table alone, with no base.
|
|
97
|
-
*
|
|
98
|
-
* Three rules, and each one closes a way to sell a bad ticket:
|
|
99
|
-
* - ticket counts sum to exactly 65,536, so the 16-bit draw is uniform;
|
|
100
|
-
* - an unused row is all-zero, so a hidden multiplier cannot ride along;
|
|
101
|
-
* - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
|
|
102
|
-
* tokens than a pack buys. This bounds tokens, not cash value.
|
|
103
|
-
*/
|
|
104
|
-
function validateTiers(tiers) {
|
|
105
|
-
if (tiers.length !== 8) throw new GaboxMathError("InvalidDistribution", `expected 8 tiers, got ${tiers.length}`);
|
|
106
|
-
let count = 0n;
|
|
107
|
-
let expected = 0n;
|
|
108
|
-
for (const [index, tier] of tiers.entries()) {
|
|
109
|
-
checkedU32(tier.tickets, `tier ${index} tickets`);
|
|
110
|
-
checkedU32(tier.multiplierBps, `tier ${index} multiplierBps`);
|
|
111
|
-
if (tier.tickets === 0) {
|
|
112
|
-
if (tier.multiplierBps !== 0) throw new GaboxMathError("InvalidDistribution", "a tier with no tickets must have no multiplier");
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (tier.multiplierBps <= 0) throw new GaboxMathError("InvalidDistribution", "a ticketed tier must pay something; it would sell a ticket that can only win zero");
|
|
116
|
-
count += BigInt(tier.tickets);
|
|
117
|
-
expected += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
118
|
-
}
|
|
119
|
-
if (count !== BigInt(65536)) throw new GaboxMathError("InvalidDistribution", `tickets must sum to ${TICKETS}, they sum to ${count}`);
|
|
120
|
-
if (expected > 10000n * BigInt(65536)) throw new GaboxMathError("UnfundedExpectation", "the expected token award exceeds the tokens a pack buys");
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* `math::validate_pack`. Every ticketed tier must pay at least one token for a pack of this size.
|
|
124
|
-
*
|
|
125
|
-
* The program checks this once at creation. At `PACK_TOKENS` no sane table fails it; it exists so
|
|
126
|
-
* a table cannot sell a ticket that can only win zero.
|
|
127
|
-
*/
|
|
128
|
-
function validatePack(packTokens, tiers) {
|
|
129
|
-
if (packTokens <= 0n) throw new GaboxMathError("ZeroAmount", "packTokens must be positive");
|
|
130
|
-
checkedU64(packTokens, "pack tokens");
|
|
131
|
-
validateTiers(tiers);
|
|
132
|
-
for (const [i, tier] of tiers.entries()) {
|
|
133
|
-
if (tier.tickets === 0) continue;
|
|
134
|
-
if (tierAmount(packTokens, tier.multiplierBps) === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* `math::seed_tokens`. The seed a table needs, in tokens.
|
|
139
|
-
*
|
|
140
|
-
* The first pack must be able to pay the largest tier in full. The pack itself brings `packTokens`
|
|
141
|
-
* into the vault, so the vault has to hold the rest beforehand:
|
|
142
|
-
*
|
|
143
|
-
* seedTokens = uncappedMaximum(packTokens, tiers) - packTokens
|
|
144
|
-
*
|
|
145
|
-
* A 5x jackpot on a 1M-token pack needs a 4M-token seed. A table whose top tier pays exactly one
|
|
146
|
-
* pack needs no seed. A top tier below one pack is refused: every ticket would lose.
|
|
147
|
-
*
|
|
148
|
-
* `initialize_pool` computes this number itself and buys exactly that many tokens on the curve.
|
|
149
|
-
* The creator only signs a maximum SOL cost.
|
|
150
|
-
*/
|
|
151
|
-
function seedTokens(packTokens, tiers) {
|
|
152
|
-
validateTiers(tiers);
|
|
153
|
-
const largest = uncappedMaximum(packTokens, tiers);
|
|
154
|
-
if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
|
|
155
|
-
return largest - packTokens;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
|
|
159
|
-
*
|
|
160
|
-
* At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
|
|
161
|
-
*/
|
|
162
|
-
function uncappedMaximum(base, tiers) {
|
|
163
|
-
validateTiers(tiers);
|
|
164
|
-
let largest = 0n;
|
|
165
|
-
for (const tier of tiers) {
|
|
166
|
-
if (tier.tickets === 0) continue;
|
|
167
|
-
const amount = tierAmount(base, tier.multiplierBps);
|
|
168
|
-
if (amount > largest) largest = amount;
|
|
169
|
-
}
|
|
170
|
-
return largest;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* `math::quote`. The offer a pack of `base` tokens would freeze right now. `base` is always
|
|
174
|
-
* `pool.packTokens`; the parameter stays general so the vectors can use small numbers.
|
|
175
|
-
*
|
|
176
|
-
* `inventory` is the vault's token balance and `reserved` is `pool.reserved`. The difference is
|
|
177
|
-
* free inventory. This pack's own `base` is added to it, because the purchase and the offer are
|
|
178
|
-
* one transaction — the tokens are in the vault before the draw is written.
|
|
179
|
-
*
|
|
180
|
-
* Every amount is capped at what is actually available. A tier that rounds to zero tokens is an
|
|
181
|
-
* error: the pool must not sell a ticket that can only win nothing.
|
|
182
|
-
*/
|
|
183
|
-
function quote(base, tiers, inventory, reserved) {
|
|
184
|
-
validateTiers(tiers);
|
|
185
|
-
if (inventory < reserved) throw new GaboxMathError("InsolventInventory", "the vault holds less than the pool has already reserved");
|
|
186
|
-
const available = checkedU64(inventory - reserved + base, "available inventory");
|
|
187
|
-
const prizes = Array.from({ length: 8 }, () => ({
|
|
188
|
-
amount: 0n,
|
|
189
|
-
tickets: 0
|
|
190
|
-
}));
|
|
191
|
-
let maximum = 0n;
|
|
192
|
-
let minimum = U64_MAX;
|
|
193
|
-
for (const [i, tier] of tiers.entries()) {
|
|
194
|
-
if (tier.tickets === 0) continue;
|
|
195
|
-
const uncapped = tierAmount(base, tier.multiplierBps);
|
|
196
|
-
if (uncapped === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
197
|
-
const amount = uncapped < available ? uncapped : available;
|
|
198
|
-
prizes[i] = {
|
|
199
|
-
amount,
|
|
200
|
-
tickets: tier.tickets
|
|
201
|
-
};
|
|
202
|
-
if (amount > maximum) maximum = amount;
|
|
203
|
-
if (amount < minimum) minimum = amount;
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
prizes,
|
|
207
|
-
maximum,
|
|
208
|
-
minimum
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* `math::choose`. Which prize a 16-bit ticket wins.
|
|
213
|
-
*
|
|
214
|
-
* The rows are consecutive ranges in table order, so this is a running total and a comparison.
|
|
215
|
-
* A ticket past the last row returns `0`, which cannot happen for a validated table.
|
|
216
|
-
*/
|
|
217
|
-
function choose(prizes, ticket) {
|
|
218
|
-
let end = 0;
|
|
219
|
-
for (const prize of prizes) {
|
|
220
|
-
end += prize.tickets;
|
|
221
|
-
if (ticket < end) return prize.amount;
|
|
222
|
-
}
|
|
223
|
-
return 0n;
|
|
224
|
-
}
|
|
225
|
-
/** `math::resolve_reservation`. Release the unwon part of a maximum, keep the award reserved. */
|
|
226
|
-
function resolveReservation(reserved, maximum, award) {
|
|
227
|
-
if (award > maximum) throw new GaboxMathError("InsolventInventory", "the award exceeds the reserved maximum");
|
|
228
|
-
if (reserved < maximum) throw new GaboxMathError("Arithmetic", "the pool has reserved less than this draw holds");
|
|
229
|
-
return checkedU64(reserved - maximum + award, "reserved");
|
|
230
|
-
}
|
|
231
|
-
/** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
|
|
232
|
-
function maxMultiplierBps(tiers) {
|
|
233
|
-
validateTiers(tiers);
|
|
234
|
-
let largest = 0;
|
|
235
|
-
for (const tier of tiers) {
|
|
236
|
-
if (tier.tickets === 0) continue;
|
|
237
|
-
if (tier.multiplierBps > largest) largest = tier.multiplierBps;
|
|
238
|
-
}
|
|
239
|
-
return largest;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* The expected multiplier over all 65,536 tickets, in basis points. Rounded down.
|
|
243
|
-
*
|
|
244
|
-
* `validateTiers` caps this at 10,000. A table at 9,800 keeps 2% of every pack's tokens in the
|
|
245
|
-
* vault on average, which is what lets a pool survive a run of top-tier wins.
|
|
246
|
-
*/
|
|
247
|
-
function averageMultiplierBps(tiers) {
|
|
248
|
-
validateTiers(tiers);
|
|
249
|
-
let weighted = 0n;
|
|
250
|
-
for (const tier of tiers) {
|
|
251
|
-
if (tier.tickets === 0) continue;
|
|
252
|
-
weighted += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
253
|
-
}
|
|
254
|
-
return Number(weighted / BigInt(TICKETS));
|
|
255
|
-
}
|
|
256
|
-
//#endregion
|
|
257
|
-
//#region src/pdas.ts
|
|
258
|
-
/**
|
|
259
|
-
* Every program-derived address a client needs.
|
|
260
|
-
*
|
|
261
|
-
* The three gabox PDAs come from the generated tree, which Codama built from the IDL's own seed
|
|
262
|
-
* metadata. Re-exported here so callers have one import, and so the seeds stay in exactly one
|
|
263
|
-
* place. The two written by hand are the ones Codama cannot generate: an ATA, and a PDA that lives
|
|
264
|
-
* under a program other than ours.
|
|
265
|
-
*/
|
|
266
|
-
/** `['referral', referee]`. A wallet's permanent referral binding. */
|
|
267
|
-
async function referralLinkAddress(referee) {
|
|
268
|
-
return (await findReferralLinkPda({ purchaser: referee }))[0];
|
|
269
|
-
}
|
|
270
|
-
/** `['referral', pool, referrer]`. A referrer's accrued rewards for one pool. */
|
|
271
|
-
async function referralAddress(pool, referrer) {
|
|
272
|
-
return (await findReferralPda({
|
|
273
|
-
pool,
|
|
274
|
-
referralLink: referrer
|
|
275
|
-
}))[0];
|
|
276
|
-
}
|
|
277
|
-
/** `["pool", mint]`. One pool per coin, and the mint alone is the seed. */
|
|
278
|
-
async function poolAddress(mint) {
|
|
279
|
-
return (await findPoolPda({ mint }))[0];
|
|
280
|
-
}
|
|
281
|
-
/** `["draw", pool, seq_u64_le]`. `seq` is `pool.nextSeq` at the moment of the purchase. */
|
|
282
|
-
async function drawAddress(pool, seq) {
|
|
283
|
-
return (await findDrawPda({
|
|
284
|
-
pool,
|
|
285
|
-
seq
|
|
286
|
-
}))[0];
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* `["identity"]` under gabox. This is the PDA gabox signs the VRF request with — the `identity`
|
|
290
|
-
* account of the `Oracle` group on `buy_pack` and `retry_draw`.
|
|
291
|
-
*/
|
|
292
|
-
async function vrfIdentityAddress() {
|
|
293
|
-
return (await findIdentityPda())[0];
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* `["identity", gabox_program_id]` under the **VRF program**. A different address from
|
|
297
|
-
* `vrfIdentityAddress`, and it belongs to the other side: MagicBlock signs the `deliver_draw`
|
|
298
|
-
* callback with it. A client never puts it in an instruction. It is here so a client can recognise
|
|
299
|
-
* the signer on a callback transaction.
|
|
300
|
-
*/
|
|
301
|
-
async function scopedVrfIdentityAddress() {
|
|
302
|
-
return await getProgramDerivedAddress({
|
|
303
|
-
programAddress: VRF_PROGRAM_ADDRESS,
|
|
304
|
-
seeds: [getBytesEncoder().encode(IDENTITY_SEED), getAddressEncoder().encode(GABOX_PROGRAM_ID)]
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* An associated token account. The seed order is `[owner, token_program, mint]`, which is the ATA
|
|
309
|
-
* program's own order and not the argument order most callers remember.
|
|
310
|
-
*/
|
|
311
|
-
async function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
|
|
312
|
-
const encoder = getAddressEncoder();
|
|
313
|
-
const [address] = await getProgramDerivedAddress({
|
|
314
|
-
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
|
|
315
|
-
seeds: [
|
|
316
|
-
encoder.encode(owner),
|
|
317
|
-
encoder.encode(tokenProgram),
|
|
318
|
-
encoder.encode(mint)
|
|
319
|
-
]
|
|
320
|
-
});
|
|
321
|
-
return address;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* The protocol fee collector's WSOL ATA, under classic SPL Token. A sale on PumpSwap pays its
|
|
325
|
-
* protocol fee here. The account must exist before the first PumpSwap sale; see `DEPLOYMENT.md`.
|
|
326
|
-
*/
|
|
327
|
-
async function feeCollectorWsolAddress() {
|
|
328
|
-
return await associatedTokenAddress(PROTOCOL_FEE_COLLECTOR, WSOL_MINT, TOKEN_PROGRAM_ADDRESS);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* The pool's prize inventory: the pool PDA's ATA for the mint's own token program.
|
|
332
|
-
*
|
|
333
|
-
* `tokenProgram` defaults to Token-2022 because Pump `create_v2` mints there, and every machine is
|
|
334
|
-
* built on a coin Pump created. Pass the mint's real owner if you have it.
|
|
335
|
-
*/
|
|
336
|
-
async function vaultAddress(mint, tokenProgram = TOKEN_2022_PROGRAM_ADDRESS) {
|
|
337
|
-
return await associatedTokenAddress(await poolAddress(mint), mint, tokenProgram);
|
|
338
|
-
}
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region src/accounts.ts
|
|
341
|
-
/**
|
|
342
|
-
* Reading gabox state: pools, draws, and the vault balance that turns a pool into an offer.
|
|
343
|
-
*
|
|
344
|
-
* The decoders are the generated ones. What this file adds is the queries — the discriminator and
|
|
345
|
-
* `memcmp` filters that let a client list every machine, or every draw a wallet is waiting on,
|
|
346
|
-
* without an indexer.
|
|
347
|
-
*
|
|
348
|
-
* # The offsets are computed, not counted
|
|
349
|
-
*
|
|
350
|
-
* A `memcmp` filter is a byte offset into an account. Counting field widths by hand is how a filter
|
|
351
|
-
* silently matches nothing. Each offset below is built from the widths of the fields before it, in
|
|
352
|
-
* the same order `state.rs` declares them, so the arithmetic is visible and a field inserted in the
|
|
353
|
-
* middle changes it.
|
|
354
|
-
*/
|
|
355
|
-
const DISCRIMINATOR = 8;
|
|
356
|
-
/** `Draw.pool` sits straight after the discriminator. */
|
|
357
|
-
const DRAW_POOL_OFFSET = DISCRIMINATOR;
|
|
358
|
-
/** `Draw.purchaser` sits after the discriminator and `pool`. */
|
|
359
|
-
const DRAW_PURCHASER_OFFSET = 40;
|
|
360
|
-
/** `Pool.creator` sits straight after the discriminator. */
|
|
361
|
-
const POOL_CREATOR_OFFSET = DISCRIMINATOR;
|
|
362
|
-
/** `Pool.mint` sits after the discriminator and `creator`. */
|
|
363
|
-
const POOL_MINT_OFFSET = 40;
|
|
364
|
-
/** `ReferralLink.referrer` sits after the discriminator and `referee`. */
|
|
365
|
-
const REFERRAL_LINK_REFERRER_OFFSET = 40;
|
|
366
|
-
const base58 = getBase58Decoder();
|
|
367
|
-
const base64$1 = getBase64Encoder();
|
|
368
|
-
/** A `memcmp` filter wants base58, and both discriminators and addresses arrive as other things. */
|
|
369
|
-
const asBase58 = (bytes) => base58.decode(bytes);
|
|
370
|
-
const memcmp = (offset, bytes) => ({ memcmp: {
|
|
371
|
-
offset: BigInt(offset),
|
|
372
|
-
bytes,
|
|
373
|
-
encoding: "base58"
|
|
374
|
-
} });
|
|
375
|
-
async function scan(rpc, filters, decode) {
|
|
376
|
-
return (await rpc.getProgramAccounts(GABOX_PROGRAM_ID, {
|
|
377
|
-
encoding: "base64",
|
|
378
|
-
commitment: "confirmed",
|
|
379
|
-
filters
|
|
380
|
-
}).send()).map(({ pubkey, account }) => decode({
|
|
381
|
-
address: pubkey,
|
|
382
|
-
data: new Uint8Array(base64$1.encode(account.data[0]))
|
|
383
|
-
}));
|
|
384
|
-
}
|
|
385
|
-
/** Wrap raw bytes in the shape the generated decoders expect. */
|
|
386
|
-
const encoded = (address, data) => ({
|
|
387
|
-
address,
|
|
388
|
-
data,
|
|
389
|
-
executable: false,
|
|
390
|
-
lamports: 0n,
|
|
391
|
-
programAddress: GABOX_PROGRAM_ID,
|
|
392
|
-
space: BigInt(data.length)
|
|
393
|
-
});
|
|
394
|
-
/** The pool for a coin, or `null` when the coin has no machine. */
|
|
395
|
-
async function fetchPoolByMint(client, mint) {
|
|
396
|
-
return await fetchPoolAt(client, await poolAddress(mint));
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* A pool by its own address, or `null`.
|
|
400
|
-
*
|
|
401
|
-
* `fetchPoolByMint` is the usual way in, because a coin's mint is the natural key. This one is for
|
|
402
|
-
* the other direction: a `Draw` names its pool and not its mint, so a client holding a draw reads
|
|
403
|
-
* the pool to learn which coin it belongs to.
|
|
404
|
-
*/
|
|
405
|
-
async function fetchPoolAt(client, address) {
|
|
406
|
-
return decodeCurrentPool(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
407
|
-
}
|
|
408
|
-
/** `null` for a missing pool; an error for one on the older devnet layout. */
|
|
409
|
-
function decodeCurrentPool(account) {
|
|
410
|
-
if (!account.exists) return null;
|
|
411
|
-
if (account.data.length !== getPoolSize()) throw new Error(`Machine ${account.address} uses an older devnet account layout and needs migration or recreation.`);
|
|
412
|
-
return decodePool(account).data;
|
|
413
|
-
}
|
|
414
|
-
/** A draw by address, or `null`. Delivered and legacy-claimed draws are closed. */
|
|
415
|
-
async function fetchDraw(client, address) {
|
|
416
|
-
const account = await fetchMaybeDraw(client.rpc, address, { commitment: "confirmed" });
|
|
417
|
-
return account.exists ? account.data : null;
|
|
418
|
-
}
|
|
419
|
-
/** Accrued referral reward for one referrer and pool, or zero when no referred pack has settled. */
|
|
420
|
-
async function fetchReferralReward(client, pool, referrer) {
|
|
421
|
-
const address = await referralAddress(pool, referrer);
|
|
422
|
-
const account = await fetchMaybeReferral(client.rpc, address, { commitment: "confirmed" });
|
|
423
|
-
return account.exists ? account.data : null;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Every current-layout machine, by discriminator and account size. Older devnet
|
|
427
|
-
* pools predate seed fields and must not be decoded with the current schema.
|
|
428
|
-
*
|
|
429
|
-
* There is no on-chain registry — the design says so on purpose — so discovery is this scan plus
|
|
430
|
-
* the `PoolCreated` event. Public RPCs limit `getProgramAccounts`, so an app that lists machines
|
|
431
|
-
* for users should cache the result rather than call this per page view.
|
|
432
|
-
*/
|
|
433
|
-
async function listPools(client) {
|
|
434
|
-
return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], ({ address, data }) => ({
|
|
435
|
-
address,
|
|
436
|
-
data: decodePool(encoded(address, data)).data
|
|
437
|
-
}));
|
|
438
|
-
}
|
|
439
|
-
/** Every draw of one pool, pending or ready. Closed draws are gone and never appear. */
|
|
440
|
-
async function listDrawsByPool(client, pool) {
|
|
441
|
-
return await listDraws(client, { pool });
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Every open draw of one wallet, across all pools. This is the "what am I owed" query.
|
|
445
|
-
*
|
|
446
|
-
* An address is already base58, so it goes into the filter unchanged.
|
|
447
|
-
*/
|
|
448
|
-
async function listDrawsByPurchaser(client, purchaser) {
|
|
449
|
-
return await listDraws(client, { purchaser });
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Open draws, narrowed by pool, by purchaser, or by both.
|
|
453
|
-
*
|
|
454
|
-
* Both filters in one scan, because a UI asks for exactly that: "my pulls on this machine". Two
|
|
455
|
-
* separate scans and an intersection in the client would move twice the bytes and could disagree
|
|
456
|
-
* with itself, since the two reads happen at different slots. With neither filter this lists every
|
|
457
|
-
* open draw of every machine.
|
|
458
|
-
*/
|
|
459
|
-
async function listDraws(client, query = {}) {
|
|
460
|
-
const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR))];
|
|
461
|
-
if (query.pool) filters.push(memcmp(8, query.pool));
|
|
462
|
-
if (query.purchaser) filters.push(memcmp(40, query.purchaser));
|
|
463
|
-
return await scan(client.rpc, filters, ({ address, data }) => ({
|
|
464
|
-
address,
|
|
465
|
-
data: decodeDraw(encoded(address, data)).data
|
|
466
|
-
}));
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Every wallet bound to one referrer. This is the "who did I refer" query.
|
|
470
|
-
*
|
|
471
|
-
* A scan, because a link is seeded on the referee and nothing on chain indexes it by referrer.
|
|
472
|
-
* The list is small in practice, and a dashboard reads it once per load, not per poll.
|
|
473
|
-
*/
|
|
474
|
-
async function listReferralLinksByReferrer(client, referrer) {
|
|
475
|
-
return await scan(client.rpc, [
|
|
476
|
-
memcmp(0, asBase58(REFERRAL_LINK_DISCRIMINATOR)),
|
|
477
|
-
{ dataSize: BigInt(getReferralLinkSize()) },
|
|
478
|
-
memcmp(40, referrer)
|
|
479
|
-
], ({ address, data }) => ({
|
|
480
|
-
address,
|
|
481
|
-
data: decodeReferralLink(encoded(address, data)).data
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
/** The vault's token balance. `0` when the vault does not exist yet. */
|
|
485
|
-
async function fetchVaultBalance(client, mint, tokenProgram) {
|
|
486
|
-
const vault = await vaultAddress(mint, tokenProgram);
|
|
487
|
-
const { value } = await client.rpc.getAccountInfo(vault, {
|
|
488
|
-
encoding: "base64",
|
|
489
|
-
commitment: "confirmed"
|
|
490
|
-
}).send();
|
|
491
|
-
if (!value) return 0n;
|
|
492
|
-
return tokenAccountAmount(new Uint8Array(base64$1.encode(value.data[0])));
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* A pool with its live inventory. This is the pair every price display needs: the tiers are
|
|
496
|
-
* immutable, but what they actually pay depends on what is free in the vault right now.
|
|
497
|
-
*
|
|
498
|
-
* # Why one request reads both accounts
|
|
499
|
-
*
|
|
500
|
-
* The oracle callback lowers the vault and `pool.reserved` in one transaction, a few seconds after
|
|
501
|
-
* a buy. Two separate reads can land on either side of it: the old `reserved` with the new, smaller
|
|
502
|
-
* vault. That pair looks insolvent, and `quote` rightly refuses it. One `getMultipleAccounts` call
|
|
503
|
-
* returns both accounts from the same slot, so the pair is always one the chain actually held.
|
|
504
|
-
*/
|
|
505
|
-
async function fetchPoolInventory(client, mint) {
|
|
506
|
-
const address = await poolAddress(mint);
|
|
507
|
-
const readPair = async (vault) => {
|
|
508
|
-
const [poolAccount, vaultAccount] = await fetchEncodedAccounts(client.rpc, [address, vault], { commitment: "confirmed" });
|
|
509
|
-
const pool = decodeCurrentPool(poolAccount);
|
|
510
|
-
if (!pool) return null;
|
|
511
|
-
return {
|
|
512
|
-
pool,
|
|
513
|
-
inventory: vaultAccount.exists ? tokenAccountAmount(vaultAccount.data) : 0n
|
|
514
|
-
};
|
|
515
|
-
};
|
|
516
|
-
const expectedVault = await vaultAddress(mint);
|
|
517
|
-
let pair = await readPair(expectedVault);
|
|
518
|
-
if (pair && pair.pool.vault !== expectedVault) pair = await readPair(pair.pool.vault);
|
|
519
|
-
if (!pair) return null;
|
|
520
|
-
const { pool, inventory } = pair;
|
|
521
|
-
const reserved = pool.reserved;
|
|
522
|
-
return {
|
|
523
|
-
pool,
|
|
524
|
-
poolAddress: address,
|
|
525
|
-
vault: pool.vault,
|
|
526
|
-
inventory,
|
|
527
|
-
reserved,
|
|
528
|
-
free: inventory >= reserved ? inventory - reserved : 0n
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
/** The generated `Tier` uses the same field names as `math.ts`, so this is only a widening. */
|
|
532
|
-
const tiersOf = (pool) => pool.tiers.map((t) => ({
|
|
533
|
-
multiplierBps: t.multiplierBps,
|
|
534
|
-
tickets: t.tickets
|
|
535
|
-
}));
|
|
536
|
-
/**
|
|
537
|
-
* The offer a pack of `base` tokens would freeze against this inventory. Pure — the same
|
|
538
|
-
* computation `buy_pack` performs, and the reason a client can show real amounts before paying.
|
|
539
|
-
*/
|
|
540
|
-
const offerFor = (inventory, base) => quote(base, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
|
|
541
|
-
//#endregion
|
|
542
|
-
//#region src/compute.ts
|
|
543
|
-
/**
|
|
544
|
-
* The two `ComputeBudget` instructions the transaction builders prepend.
|
|
545
|
-
*
|
|
546
|
-
* # Why every gabox transaction needs one
|
|
547
|
-
*
|
|
548
|
-
* The default budget is 200,000 compute units for the whole transaction. `buy_pack` alone does a
|
|
549
|
-
* CPI into Pump (which does its own CPIs into two token programs and its fee program), a
|
|
550
|
-
* `transfer_checked`, an account init, a system transfer, and a CPI into MagicBlock's VRF program
|
|
551
|
-
* that allocates a request account. `createMachine` adds Pump's `create_v2` to that, which mints a
|
|
552
|
-
* Token-2022 coin with metadata. Neither fits in the default, and nothing on chain can raise its
|
|
553
|
-
* own budget.
|
|
554
|
-
*
|
|
555
|
-
* # The numbers are headroom, not measurements
|
|
556
|
-
*
|
|
557
|
-
* They have not been measured on chain. They sit comfortably above what these instruction mixes
|
|
558
|
-
* plausibly cost and comfortably below the 1,400,000 ceiling. **Measure them on devnet before they
|
|
559
|
-
* matter**, because the prioritisation fee is `price x requested limit`: a request that is three
|
|
560
|
-
* times too large costs three times too much on every pack. Until then, treat them as a starting
|
|
561
|
-
* point a caller may override.
|
|
562
|
-
*/
|
|
563
|
-
const COMPUTE_BUDGET_PROGRAM_ADDRESS = "ComputeBudget111111111111111111111111111111";
|
|
564
|
-
/** The runtime's per-transaction ceiling. A larger request is rejected outright. */
|
|
565
|
-
const MAX_COMPUTE_UNIT_LIMIT = 14e5;
|
|
566
|
-
/** What an instruction gets when no `SetComputeUnitLimit` is present. */
|
|
567
|
-
const DEFAULT_COMPUTE_UNIT_LIMIT = 2e5;
|
|
568
|
-
/**
|
|
569
|
-
* UNVERIFIED: none of the three figures below has been measured on chain. They are headroom, chosen
|
|
570
|
-
* above what these instruction mixes plausibly cost. Measure them on devnet before they matter: the
|
|
571
|
-
* prioritisation fee is `price x requested limit`, so a request three times too large costs three
|
|
572
|
-
* times too much on every pack.
|
|
573
|
-
*/
|
|
574
|
-
/** Pump `create_v2` plus `initialize_pool` plus a seed buy, in one transaction. */
|
|
575
|
-
const CREATE_MACHINE_COMPUTE_UNITS = 6e5;
|
|
576
|
-
/** A venue buy, an escrow transfer, a draw init and a VRF request. */
|
|
577
|
-
const BUY_PACK_COMPUTE_UNITS = 5e5;
|
|
578
|
-
/** A vault transfer, or a vault transfer plus a venue sale. */
|
|
579
|
-
const REDEEM_COMPUTE_UNITS = 3e5;
|
|
580
|
-
/** `bind_referrer` riding ahead of a pack: one small account init. */
|
|
581
|
-
const BIND_REFERRER_COMPUTE_UNITS = 5e4;
|
|
582
|
-
/** `ComputeBudgetInstruction`'s discriminants. Positional and append-only upstream. */
|
|
583
|
-
const SET_COMPUTE_UNIT_LIMIT = 2;
|
|
584
|
-
const SET_COMPUTE_UNIT_PRICE = 3;
|
|
585
|
-
/** `[u8 discriminant, u32 units]` — five bytes. */
|
|
586
|
-
const LIMIT_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["units", getU32Encoder()]]);
|
|
587
|
-
/** `[u8 discriminant, u64 microLamports]` — nine bytes. */
|
|
588
|
-
const PRICE_ENCODER = getStructEncoder([["discriminant", getU8Encoder()], ["microLamports", getU64Encoder()]]);
|
|
589
|
-
function getSetComputeUnitLimitInstruction(units) {
|
|
590
|
-
if (!Number.isInteger(units) || units < 0 || units > 14e5) throw new Error(`compute unit limit must be an integer in 0..=${MAX_COMPUTE_UNIT_LIMIT}, got ${units}`);
|
|
591
|
-
return {
|
|
592
|
-
programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
|
|
593
|
-
accounts: [],
|
|
594
|
-
data: LIMIT_ENCODER.encode({
|
|
595
|
-
discriminant: SET_COMPUTE_UNIT_LIMIT,
|
|
596
|
-
units
|
|
597
|
-
})
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* `SetComputeUnitPrice(microLamports)` — the priority fee, per compute unit.
|
|
602
|
-
*
|
|
603
|
-
* No default, deliberately. The fee that lands a transaction is a property of the network at the
|
|
604
|
-
* moment you send it. A hardcoded price is either money burnt on an idle chain or a transaction
|
|
605
|
-
* that quietly stops landing under load. Sample `getRecentPrioritizationFees`, or take it from
|
|
606
|
-
* config.
|
|
607
|
-
*/
|
|
608
|
-
function getSetComputeUnitPriceInstruction(microLamports) {
|
|
609
|
-
const price = BigInt(microLamports);
|
|
610
|
-
if (price < 0n) throw new Error(`compute unit price must not be negative, got ${price}`);
|
|
611
|
-
return {
|
|
612
|
-
programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
|
|
613
|
-
accounts: [],
|
|
614
|
-
data: PRICE_ENCODER.encode({
|
|
615
|
-
discriminant: SET_COMPUTE_UNIT_PRICE,
|
|
616
|
-
microLamports: price
|
|
617
|
-
})
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
/** The compute budget prefix a builder prepends: a limit, and a price only when one is asked for. */
|
|
621
|
-
function computeBudgetInstructions(units, microLamports) {
|
|
622
|
-
const instructions = [getSetComputeUnitLimitInstruction(units)];
|
|
623
|
-
if (microLamports !== void 0) instructions.push(getSetComputeUnitPriceInstruction(microLamports));
|
|
624
|
-
return instructions;
|
|
625
|
-
}
|
|
626
|
-
//#endregion
|
|
627
7
|
//#region src/events.ts
|
|
628
|
-
/**
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
* # Where the events are
|
|
632
|
-
*
|
|
633
|
-
* Anchor's `emit!` writes the event through `sol_log_data`, which the runtime renders as a log line
|
|
634
|
-
* `Program data: <base64>`. So decoding an event is: find those lines, base64-decode each one,
|
|
635
|
-
* match the first eight bytes against a discriminator, and hand the rest to the generated decoder.
|
|
636
|
-
*
|
|
637
|
-
* The design leans on this. There is no on-chain market registry, so `PoolCreated` is how a new
|
|
638
|
-
* machine is discovered, and `DrawResolved` stays readable after the `Draw` account is closed and
|
|
639
|
-
* its rent returned. The event is the audit trail; the account is only the pending state.
|
|
640
|
-
*
|
|
641
|
-
* # Waiting
|
|
642
|
-
*
|
|
643
|
-
* `watchDraw` subscribes to the draw account. A normal callback, retry callback, or expiry
|
|
644
|
-
* delivers the prize and closes that account; the watcher then reconstructs the result from the
|
|
645
|
-
* transaction's `PackBought` and `DrawResolved` events. It also accepts a legacy open `Ready`
|
|
646
|
-
* account from a callback requested before automatic delivery.
|
|
647
|
-
*/
|
|
648
|
-
const base64 = getBase64Encoder();
|
|
649
|
-
const startsWith = (data, discriminator) => {
|
|
650
|
-
if (data.length < discriminator.length) return false;
|
|
651
|
-
for (let i = 0; i < discriminator.length; i++) if (data[i] !== discriminator[i]) return false;
|
|
652
|
-
return true;
|
|
653
|
-
};
|
|
654
|
-
/** The prefix Anchor's `emit!` produces. Anything else in the log is not an event. */
|
|
655
|
-
const PROGRAM_DATA = "Program data: ";
|
|
656
|
-
/** Decode one `Program data:` payload, or `null` when it is not one of ours. */
|
|
8
|
+
/** Decode Gabox's Anchor events. Draw accounts are closed on delivery; use `DrawResolved` as final state. */
|
|
9
|
+
const b64 = getBase64Encoder();
|
|
10
|
+
const starts = (a, b) => a.length >= b.length && b.every((v, i) => a[i] === v);
|
|
657
11
|
function decodeEvent(data) {
|
|
658
|
-
if (
|
|
12
|
+
if (starts(data, POOL_CREATED_EVENT_DISCRIMINATOR)) return {
|
|
659
13
|
name: "PoolCreated",
|
|
660
14
|
data: getPoolCreatedEventDecoder().decode(data)
|
|
661
15
|
};
|
|
662
|
-
if (
|
|
16
|
+
if (starts(data, PRIZES_FUNDED_EVENT_DISCRIMINATOR)) return {
|
|
663
17
|
name: "PrizesFunded",
|
|
664
18
|
data: getPrizesFundedEventDecoder().decode(data)
|
|
665
19
|
};
|
|
666
|
-
if (
|
|
20
|
+
if (starts(data, PACK_BOUGHT_EVENT_DISCRIMINATOR)) return {
|
|
667
21
|
name: "PackBought",
|
|
668
22
|
data: getPackBoughtEventDecoder().decode(data)
|
|
669
23
|
};
|
|
670
|
-
if (
|
|
24
|
+
if (starts(data, RANDOMNESS_RETRIED_EVENT_DISCRIMINATOR)) return {
|
|
671
25
|
name: "RandomnessRetried",
|
|
672
26
|
data: getRandomnessRetriedEventDecoder().decode(data)
|
|
673
27
|
};
|
|
674
|
-
if (
|
|
28
|
+
if (starts(data, DRAW_RESOLVED_EVENT_DISCRIMINATOR)) return {
|
|
675
29
|
name: "DrawResolved",
|
|
676
30
|
data: getDrawResolvedEventDecoder().decode(data)
|
|
677
31
|
};
|
|
678
|
-
if (
|
|
679
|
-
name: "PrizeRedeemed",
|
|
680
|
-
data: getPrizeRedeemedEventDecoder().decode(data)
|
|
681
|
-
};
|
|
682
|
-
if (startsWith(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
|
|
32
|
+
if (starts(data, TOKENS_SOLD_EVENT_DISCRIMINATOR)) return {
|
|
683
33
|
name: "TokensSold",
|
|
684
34
|
data: getTokensSoldEventDecoder().decode(data)
|
|
685
35
|
};
|
|
686
36
|
return null;
|
|
687
37
|
}
|
|
38
|
+
const BASE58 = "[1-9A-HJ-NP-Za-km-z]+";
|
|
39
|
+
const INVOKE = new RegExp(`^Program (${BASE58}) invoke \\[(\\d+)\\]$`);
|
|
40
|
+
const SUCCESS = new RegExp(`^Program (${BASE58}) success$`);
|
|
41
|
+
const FAILED = new RegExp(`^Program (${BASE58}) failed: .*$`);
|
|
688
42
|
/**
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
* rather than reported. Another program in the same transaction emits its own events, and they are
|
|
693
|
-
* not an error here.
|
|
43
|
+
* Decode only Gabox events committed by successful runtime frames. Program logs are emitted before
|
|
44
|
+
* transaction commit and are forgeable by arbitrary programs, so `Program data` is authenticated
|
|
45
|
+
* by the canonical invoke/success stack and buffered until every enclosing frame succeeds.
|
|
694
46
|
*/
|
|
695
47
|
function decodeEvents(logs) {
|
|
696
|
-
const
|
|
48
|
+
const committed = [];
|
|
49
|
+
const stack = [];
|
|
50
|
+
const discardOpenFrames = () => {
|
|
51
|
+
stack.length = 0;
|
|
52
|
+
};
|
|
53
|
+
let malformed = false;
|
|
54
|
+
let transactionFailed = false;
|
|
697
55
|
for (const line of logs) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
56
|
+
const invoke = INVOKE.exec(line);
|
|
57
|
+
if (invoke) {
|
|
58
|
+
if (Number(invoke[2]) !== stack.length + 1) {
|
|
59
|
+
malformed = true;
|
|
60
|
+
discardOpenFrames();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
stack.push({
|
|
64
|
+
programId: invoke[1],
|
|
65
|
+
pending: []
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const success = SUCCESS.exec(line);
|
|
70
|
+
const failed = FAILED.exec(line);
|
|
71
|
+
if (success || failed) {
|
|
72
|
+
const programId = (success ?? failed)[1];
|
|
73
|
+
const frame = stack.at(-1);
|
|
74
|
+
if (!frame || frame.programId !== programId) {
|
|
75
|
+
malformed = true;
|
|
76
|
+
discardOpenFrames();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
stack.pop();
|
|
80
|
+
if (success) {
|
|
81
|
+
const parent = stack.at(-1);
|
|
82
|
+
if (parent) parent.pending.push(...frame.pending);
|
|
83
|
+
else committed.push(...frame.pending);
|
|
84
|
+
}
|
|
85
|
+
if (failed && stack.length === 0) transactionFailed = true;
|
|
704
86
|
continue;
|
|
705
87
|
}
|
|
706
|
-
const
|
|
707
|
-
if (
|
|
88
|
+
const frame = stack.at(-1);
|
|
89
|
+
if (!line.startsWith("Program data: ") || frame?.programId !== GABOX_PROGRAM_ID) continue;
|
|
90
|
+
try {
|
|
91
|
+
const event = decodeEvent(new Uint8Array(b64.encode(line.slice(14).trim())));
|
|
92
|
+
if (event) frame.pending.push(event);
|
|
93
|
+
} catch {}
|
|
708
94
|
}
|
|
709
|
-
return
|
|
95
|
+
return malformed || transactionFailed || stack.length !== 0 ? [] : committed;
|
|
710
96
|
}
|
|
711
|
-
/** Fetch a transaction and decode the gabox events it emitted. */
|
|
712
97
|
async function fetchEvents(client, signature) {
|
|
713
98
|
return await readEvents(client.rpc, signature);
|
|
714
99
|
}
|
|
715
|
-
/** The RPC half of `fetchEvents`, for the internal readers that already hold one. */
|
|
716
100
|
async function readEvents(rpc, signature) {
|
|
717
|
-
|
|
101
|
+
const tx = await rpc.getTransaction(signature, {
|
|
718
102
|
commitment: "confirmed",
|
|
719
103
|
encoding: "json",
|
|
720
104
|
maxSupportedTransactionVersion: 0
|
|
721
|
-
}).send()
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const current = await readDraw(rpc, address);
|
|
744
|
-
if (current) {
|
|
745
|
-
options.onChange?.(current);
|
|
746
|
-
if (current.status === 1) return current;
|
|
747
|
-
} else {
|
|
748
|
-
const delivered = await readDeliveredDraw(rpc, address);
|
|
749
|
-
if (delivered) return delivered;
|
|
750
|
-
}
|
|
751
|
-
for await (const notification of notifications) {
|
|
752
|
-
const account = notification.value;
|
|
753
|
-
if (!account || account.data[0] === "") {
|
|
754
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
755
|
-
const delivered = await readDeliveredDraw(rpc, address);
|
|
756
|
-
if (delivered) return delivered;
|
|
757
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
758
|
-
}
|
|
759
|
-
throw new Error(`draw ${address} closed before its resolution event became readable`);
|
|
105
|
+
}).send();
|
|
106
|
+
const meta = tx?.meta;
|
|
107
|
+
if (!tx || !meta || meta.err) return [];
|
|
108
|
+
return decodeEvents(meta.logMessages ?? []);
|
|
109
|
+
}
|
|
110
|
+
/** Poll final resolution events for one closed draw address. There is no separate Ready/claim state to poll instead. */
|
|
111
|
+
async function findResolvedDraw(client, address) {
|
|
112
|
+
let before;
|
|
113
|
+
for (let page = 0; page < 10; page++) {
|
|
114
|
+
const rows = await client.rpc.getSignaturesForAddress(address, {
|
|
115
|
+
commitment: "confirmed",
|
|
116
|
+
limit: 100,
|
|
117
|
+
...before ? { before } : {}
|
|
118
|
+
}).send();
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
if (row.err) continue;
|
|
121
|
+
for (const event of await readEvents(client.rpc, row.signature)) {
|
|
122
|
+
if (event.name !== "DrawResolved") continue;
|
|
123
|
+
if (await drawAddress(event.data.pool, event.data.seq) === address) return {
|
|
124
|
+
...event.data,
|
|
125
|
+
address
|
|
126
|
+
};
|
|
760
127
|
}
|
|
761
|
-
const draw = decodeDrawBytes(address, new Uint8Array(base64.encode(account.data[0])));
|
|
762
|
-
options.onChange?.(draw);
|
|
763
|
-
if (draw.status === 1) return draw;
|
|
764
128
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
controller.abort();
|
|
129
|
+
if (rows.length < 100) return null;
|
|
130
|
+
before = rows.at(-1)?.signature;
|
|
131
|
+
if (!before) return null;
|
|
769
132
|
}
|
|
770
|
-
|
|
771
|
-
function decodeDrawBytes(address, data) {
|
|
772
|
-
return decodeDraw({
|
|
773
|
-
address,
|
|
774
|
-
data,
|
|
775
|
-
executable: false,
|
|
776
|
-
lamports: 0n,
|
|
777
|
-
programAddress: GABOX_PROGRAM_ID,
|
|
778
|
-
space: BigInt(data.length)
|
|
779
|
-
}).data;
|
|
780
|
-
}
|
|
781
|
-
async function readDraw(rpc, address) {
|
|
782
|
-
const { value } = await rpc.getAccountInfo(address, {
|
|
783
|
-
encoding: "base64",
|
|
784
|
-
commitment: "confirmed"
|
|
785
|
-
}).send();
|
|
786
|
-
if (!value) return null;
|
|
787
|
-
return decodeDrawBytes(address, new Uint8Array(base64.encode(value.data[0])));
|
|
788
|
-
}
|
|
789
|
-
/** Recover the final snapshot after automatic delivery closed the draw account. */
|
|
790
|
-
async function readDeliveredDraw(rpc, address) {
|
|
791
|
-
const signatures = await rpc.getSignaturesForAddress(address, {
|
|
792
|
-
commitment: "confirmed",
|
|
793
|
-
limit: 10
|
|
794
|
-
}).send();
|
|
795
|
-
const rows = await Promise.all(signatures.filter((row) => !row.err).map(async (row) => ({
|
|
796
|
-
slot: row.slot,
|
|
797
|
-
events: await readEvents(rpc, row.signature)
|
|
798
|
-
})));
|
|
799
|
-
const bought = rows.flatMap((row) => row.events.flatMap((event) => event.name === "PackBought" ? [{
|
|
800
|
-
slot: row.slot,
|
|
801
|
-
data: event.data
|
|
802
|
-
}] : []))[0];
|
|
803
|
-
const resolved = rows.flatMap((row) => row.events.flatMap((event) => event.name === "DrawResolved" ? [{
|
|
804
|
-
slot: row.slot,
|
|
805
|
-
data: event.data
|
|
806
|
-
}] : []))[0];
|
|
807
|
-
if (!bought || !resolved) return null;
|
|
808
|
-
if (bought.data.pool !== resolved.data.pool || bought.data.seq !== resolved.data.seq || bought.data.purchaser !== resolved.data.purchaser) return null;
|
|
809
|
-
const [derived, bump] = await findDrawPda({
|
|
810
|
-
pool: bought.data.pool,
|
|
811
|
-
seq: bought.data.seq
|
|
812
|
-
});
|
|
813
|
-
if (derived !== address) return null;
|
|
814
|
-
const ticketed = bought.data.prizes.filter((prize) => prize.tickets > 0);
|
|
815
|
-
if (ticketed.length === 0) return null;
|
|
816
|
-
const lastRetry = rows.flatMap((row) => row.events.flatMap((event) => event.name === "RandomnessRetried" && event.data.pool === bought.data.pool && event.data.seq === bought.data.seq ? [{
|
|
817
|
-
slot: row.slot,
|
|
818
|
-
attempt: event.data.attempt
|
|
819
|
-
}] : [])).reduce((latest, row) => !latest || row.slot > latest.slot ? row : latest, null);
|
|
820
|
-
return {
|
|
821
|
-
discriminator: DRAW_DISCRIMINATOR,
|
|
822
|
-
pool: bought.data.pool,
|
|
823
|
-
purchaser: bought.data.purchaser,
|
|
824
|
-
seq: bought.data.seq,
|
|
825
|
-
bump,
|
|
826
|
-
status: 1,
|
|
827
|
-
requestSlot: bought.slot,
|
|
828
|
-
lastAttemptSlot: lastRetry?.slot ?? bought.slot,
|
|
829
|
-
attempts: lastRetry?.attempt ?? 1,
|
|
830
|
-
maximum: ticketed.reduce((a, prize) => a > prize.amount ? a : prize.amount, 0n),
|
|
831
|
-
minimum: ticketed.reduce((a, prize) => a < prize.amount ? a : prize.amount, ticketed[0].amount),
|
|
832
|
-
prizes: bought.data.prizes,
|
|
833
|
-
amount: resolved.data.amount,
|
|
834
|
-
randomness: resolved.data.randomness,
|
|
835
|
-
timedOut: resolved.data.timedOut
|
|
836
|
-
};
|
|
133
|
+
return null;
|
|
837
134
|
}
|
|
838
135
|
//#endregion
|
|
839
136
|
//#region src/lookupTables.ts
|
|
840
137
|
/**
|
|
841
|
-
* Shared devnet address lookup table, verified on 2026-09-
|
|
138
|
+
* Shared devnet address lookup table, verified on 2026-09-18T11:04:06.369Z.
|
|
842
139
|
* Generated by scripts/deploy-lookup-table.ts. Existing indices are immutable;
|
|
843
140
|
* keep this table active while clients use it. Authority is the devnet deploy wallet.
|
|
844
141
|
*/
|
|
@@ -917,7 +214,54 @@ const DEVNET_LOOKUP_TABLE_ADDRESSES = [
|
|
|
917
214
|
address("Freijj9xKLefjrb5fHgT6KMbYG1XBP2mA83tqeXYUMYM"),
|
|
918
215
|
address("4uzPz9TPskXiiEZ6X78rqud8LvfdxkJBr5EKHgbx4azP"),
|
|
919
216
|
address("Hxzab4UjjVH2KjsdAqzdxGdYUpNN5FKhpu7iikB869uH"),
|
|
920
|
-
address("Frkwunr9dQM9d4TthfQ2unxC994XpkLMpkRP2e7yfirk")
|
|
217
|
+
address("Frkwunr9dQM9d4TthfQ2unxC994XpkLMpkRP2e7yfirk"),
|
|
218
|
+
address("E6ShohW57z5CJPBeEcFAEbvPqUyt6QxHcxnkh4hMaNrg"),
|
|
219
|
+
address("6z6GDdfb2AjR9ZhJmAUQ5cipJCVxQvLJhB2H8mCwTFBP"),
|
|
220
|
+
address("D9LwzTvJ5XoGxorcdaZvBgq7Qruqewu2P9WVsgSrKURd"),
|
|
221
|
+
address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"),
|
|
222
|
+
address("GCow1cTVRa43v8EX1eVwVobiKXoyZjfQpwAvgpZ8B2KP"),
|
|
223
|
+
address("65LkFkYo9gMD6AwXTbSsxR3d28pCVbJp5AtE9NK3634n"),
|
|
224
|
+
address("8wDZLhae9WbNvm3eCgHVDrXUhJbd3gx6DnTnhsw45XEj"),
|
|
225
|
+
address("CPgoAkfWjiUZfNLRp94hD7BjmDSFy96mS8xcQKoPFB4H"),
|
|
226
|
+
address("GXVKyyXsoUihF1uzGCx7YXVqQ7kimUDAo3xyqULvgDyB"),
|
|
227
|
+
address("5tpFHeni6NYD6pQpkuReVPGtCJLyaXDq97FTJJzyZMjt"),
|
|
228
|
+
address("DNscAYMk2LW55Aq7FTj24mRbwVPZ6EgvCUoabhhtdVCW"),
|
|
229
|
+
address("7Q2wpniGesAyAkjACpg58BAcDjBKcdcX9a6RAgszVh4M"),
|
|
230
|
+
address("7gx9ZEwMSd1ifHBEgVaUsCkQmHTFJTWVs3C1bQABQ22T"),
|
|
231
|
+
address("GsVBKjffkB769p9tHTZWoAX3r9T6dXoDTJr3f7XutJH7"),
|
|
232
|
+
address("9NFrxdnmedHKHs1tnhYm9G5XTJh7Lt7xN6uxgZzwQNM7"),
|
|
233
|
+
address("BhMknQ4j9RZUbJk6GS4QJh8MSKxcqZHS2x9MZh2AH9hA"),
|
|
234
|
+
address("9NW32ymMo8Qx6DTbgkxtnD8Dh9hssQYpcyY2Brdpg2hs"),
|
|
235
|
+
address("5a8Gfgwx4hrCtYKgvjtX57FsirXPRN7Jzm9aXmn6hQs8"),
|
|
236
|
+
address("5KUNmCZatysY7fxLtTgo2bpkqevPRZZG8fkrh3e1P89F"),
|
|
237
|
+
address("HPfEytxa5JGqmGiVwrSepAcNTkWboxvtQKbyWN9DNCoQ"),
|
|
238
|
+
address("Gr5kHfDBd7GAdjK6Ct3EDC566XFPjCr3mLCkKVxJYrMD"),
|
|
239
|
+
address("HzCwuA6T48enyWvCJWrLhNvMpJJR43kddjMGFf6LyA9L"),
|
|
240
|
+
address("Do4esSd37h4uHz35piua8rRtcXDNt27re8NDqqZfjsGJ"),
|
|
241
|
+
address("2GC6K75FS6MpRrMZYXifinC2sVXP3htsJpayerx7ACci"),
|
|
242
|
+
address("FvEpzodkyvMzRHQo4q7fhvfa287hnjfr1oiumRtQXrse"),
|
|
243
|
+
address("8SwxZnhYHeC9S93ZFzLTd9dC2c44hfCdGyoXYzA7U6Db"),
|
|
244
|
+
address("6B1oS2LSDYHyXpRF9S8C6r5LffitosDrhie2WWsUtfRV"),
|
|
245
|
+
address("F2pzDCn3vqXcNWF6osLxSvVtf3CyTkHd4tRxhGjyRZQ9"),
|
|
246
|
+
address("DKESpHxobrT9Ra4snSXCD4cdhQMqpzmGhZN1HZ3sUYMY"),
|
|
247
|
+
address("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
|
|
248
|
+
address("SysvarRent111111111111111111111111111111111"),
|
|
249
|
+
address("DRay6fNdQ5J82H7xV6uq2aV3mNrUZ1J4PgSKsWgptcm6"),
|
|
250
|
+
address("5xqNaZXX5eUi4p5HU4oz9i5QnwRNT2y6oN7yyn4qENeq"),
|
|
251
|
+
address("4uAB7seenFJKPUXqYewAdfra2u6baBgjiXU8x1SC7Ycz"),
|
|
252
|
+
address("7ZR4zD7PYfY2XxoG1Gxcy2EgEeGYrpxrwzPuwdUBssEt"),
|
|
253
|
+
address("DdEeCPXbCAzHE2PZSoR3RZng4WA4bSztrezQznrJ4ooB"),
|
|
254
|
+
address("DRaycpLY18LhpbydsBWbVJtxpNv9oXPgjRSfpF2bWpYb"),
|
|
255
|
+
address("CXniRufdq5xL8t8jZAPxsPZDpuudwuJSPWnbcD5Y5Nxq"),
|
|
256
|
+
address("G7YfJJp1TX1VtzN4V2yhPNSU23AKPSy1U2miRdwAByK5"),
|
|
257
|
+
address("5WcPTEQ59UqpQzjZUPbU8QRGCbj7NeQNLDa7DbsLkLKT"),
|
|
258
|
+
address("USDCoctVLVnvTXBEuP9s8hntucdJokbo17RwHuNXemT"),
|
|
259
|
+
address("4wHbNkobu7iARU9MbCEqDSAq6JuQreGupG2Jsf2R3DFP"),
|
|
260
|
+
address("5Eu2G2USTy1pqphmQzQ2SBXWrBq5sdhgEh7hso9R2xix"),
|
|
261
|
+
address("A9qBhPy4k5UYW72hSgAkh1Epr2do69P54yzzcMV3yv6b"),
|
|
262
|
+
address("Aw93pmXP52u6WSW2HcafRxua1LDht5MZhhXaaR7qCjsN"),
|
|
263
|
+
address("CPLUA2NTYSGjsB1E9iXT3MrPn69WRFJvKTdJZw5NdEjh"),
|
|
264
|
+
address("7LnqjXdqJEdccWZQs5YJobQ8MDmcK4sG2oo4Ty4LBC8c")
|
|
921
265
|
];
|
|
922
266
|
const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET_LOOKUP_TABLE_ADDRESSES] };
|
|
923
267
|
/**
|
|
@@ -931,67 +275,614 @@ const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET
|
|
|
931
275
|
function defaultAddressLookupTables(cluster) {
|
|
932
276
|
return cluster === "devnet" ? { ...DEVNET_ADDRESS_LOOKUP_TABLES } : {};
|
|
933
277
|
}
|
|
278
|
+
/** The address lookup table program. It owns every table account. */
|
|
279
|
+
const LOOKUP_TABLE_PROGRAM = address("AddressLookupTab1e1111111111111111111111111");
|
|
280
|
+
/**
|
|
281
|
+
* The fixed part of a table account, before its addresses: a 4-byte discriminator, the two slot
|
|
282
|
+
* fields, the start index, the optional authority, and two padding bytes.
|
|
283
|
+
*/
|
|
284
|
+
const LOOKUP_TABLE_HEADER = 56;
|
|
285
|
+
/**
|
|
286
|
+
* Read lookup tables off chain by address, for compressing against tables this SDK does not pin.
|
|
287
|
+
*
|
|
288
|
+
* A router picks its own tables per quote, so their contents are only known at run time, and a
|
|
289
|
+
* message can only be compressed against a table whose addresses are loaded. An address with no
|
|
290
|
+
* account, a wrong owner, or a malformed body is skipped rather than failing the whole route: the
|
|
291
|
+
* message then carries those accounts in full, which is correct, only larger.
|
|
292
|
+
*/
|
|
293
|
+
async function fetchAddressLookupTables(client, addresses) {
|
|
294
|
+
const wanted = [...new Set(addresses)];
|
|
295
|
+
if (wanted.length === 0) return {};
|
|
296
|
+
const { value } = await client.rpc.getMultipleAccounts(wanted, {
|
|
297
|
+
encoding: "base64",
|
|
298
|
+
commitment: "confirmed"
|
|
299
|
+
}).send();
|
|
300
|
+
const decoder = getAddressDecoder();
|
|
301
|
+
const tables = {};
|
|
302
|
+
for (const [index, account] of value.entries()) {
|
|
303
|
+
if (!account || account.owner !== LOOKUP_TABLE_PROGRAM) continue;
|
|
304
|
+
const data = Buffer.from(account.data[0], "base64");
|
|
305
|
+
const body = data.length - LOOKUP_TABLE_HEADER;
|
|
306
|
+
if (body <= 0 || body % 32 !== 0) continue;
|
|
307
|
+
const stored = [];
|
|
308
|
+
for (let at = LOOKUP_TABLE_HEADER; at < data.length; at += 32) stored.push(decoder.decode(new Uint8Array(data.subarray(at, at + 32))));
|
|
309
|
+
tables[wanted[index]] = stored;
|
|
310
|
+
}
|
|
311
|
+
return tables;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/route/leg.ts
|
|
315
|
+
/**
|
|
316
|
+
* The margin added to an exact-in fallback, in basis points.
|
|
317
|
+
*
|
|
318
|
+
* An exact-in quote prices one spend. The spend that buys the amount wanted is worked out from that
|
|
319
|
+
* price, and the price moves against a larger spend, so the result is always a little short without
|
|
320
|
+
* a margin. 1% is the same order as the slippage a caller already signs for on the pack itself.
|
|
321
|
+
*/
|
|
322
|
+
const EXACT_IN_MARGIN_BPS = 100n;
|
|
323
|
+
/**
|
|
324
|
+
* The first exact-in quote's size, in lamports, when a pair has no exact-out route.
|
|
325
|
+
*
|
|
326
|
+
* It exists only to learn a price, so it is small enough that its own impact on the route is
|
|
327
|
+
* small, and large enough that a route quotes it at all. The real spend is worked out from the
|
|
328
|
+
* price it returns and re-quoted.
|
|
329
|
+
*/
|
|
330
|
+
const PROBE_LAMPORTS = 100000000n;
|
|
331
|
+
/** How many times the exact-in fallback re-quotes before it gives up. */
|
|
332
|
+
const EXACT_IN_ATTEMPTS = 3;
|
|
333
|
+
/**
|
|
334
|
+
* A swap that leaves at least `amount` of `quoteMint` in the user's quote account, paid for in SOL.
|
|
335
|
+
*
|
|
336
|
+
* Exact-out when the pair has such a route, so the buyer spends only what the pack costs. Exact-in
|
|
337
|
+
* otherwise, which overshoots on purpose: the leftover quote stays in the buyer's own account.
|
|
338
|
+
*/
|
|
339
|
+
async function routeQuoteIn(client, provider, input) {
|
|
340
|
+
const { quoteMint, amount, user } = input;
|
|
341
|
+
if (amount <= 0n) throw new Error("the quote amount to buy must be positive");
|
|
342
|
+
if (quoteMint === "So11111111111111111111111111111111111111112") throw new Error("a WSOL-quoted pool needs no route: the builders wrap SOL themselves");
|
|
343
|
+
try {
|
|
344
|
+
const route = await provider.exactOut(client, WSOL_MINT, quoteMint, amount, user);
|
|
345
|
+
if (route.outAmount < amount) throw new Error(`the exact-out route buys ${route.outAmount} of ${quoteMint}, which is below the ${amount} the pack costs`);
|
|
346
|
+
return route;
|
|
347
|
+
} catch (exactOutFailure) {
|
|
348
|
+
return await exactInFallback(client, provider, quoteMint, amount, user, exactOutFailure);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Work out the SOL that buys `amount` of the quote at the exact-in price, then swap it.
|
|
353
|
+
*
|
|
354
|
+
* The first quote is a small probe, only to learn a price. Every later quote scales the last one by
|
|
355
|
+
* what it actually returned, so an impact the probe did not show is corrected rather than guessed
|
|
356
|
+
* at. Three quotes at most, and a route that still falls short is an error rather than a buy that
|
|
357
|
+
* fails on chain.
|
|
358
|
+
*/
|
|
359
|
+
async function exactInFallback(client, provider, quoteMint, amount, user, exactOutFailure) {
|
|
360
|
+
let spend = PROBE_LAMPORTS;
|
|
361
|
+
let last;
|
|
362
|
+
for (let attempt = 0; attempt < EXACT_IN_ATTEMPTS; attempt++) {
|
|
363
|
+
let route;
|
|
364
|
+
try {
|
|
365
|
+
route = await provider.exactIn(client, WSOL_MINT, quoteMint, spend, user);
|
|
366
|
+
} catch (exactInFailure) {
|
|
367
|
+
throw new Error(`no route from SOL to ${quoteMint}. Exact-out failed with "${messageOf(exactOutFailure)}" and exact-in with "${messageOf(exactInFailure)}".`);
|
|
368
|
+
}
|
|
369
|
+
last = route;
|
|
370
|
+
if (route.outAmount >= amount) return route;
|
|
371
|
+
if (route.outAmount <= 0n) break;
|
|
372
|
+
const scaled = ceilDiv(route.inAmount * amount, route.outAmount);
|
|
373
|
+
const next = scaled + scaled * EXACT_IN_MARGIN_BPS / 10000n;
|
|
374
|
+
if (next <= spend) break;
|
|
375
|
+
spend = next;
|
|
376
|
+
}
|
|
377
|
+
throw new Error(`no route from SOL to ${quoteMint} buys ${amount}. The best exact-in quote returned ${last?.outAmount ?? 0n} for ${last?.inAmount ?? spend} lamports, and exact-out failed with "${messageOf(exactOutFailure)}".`);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* A swap that turns exactly `amount` of `quoteMint` into SOL.
|
|
381
|
+
*
|
|
382
|
+
* `sellTokens` uses it on the proceeds floor it already signs for, so the amount swapped is one the
|
|
383
|
+
* sale is guaranteed to have produced. Anything the sale paid above that floor stays in the
|
|
384
|
+
* seller's quote account.
|
|
385
|
+
*/
|
|
386
|
+
async function routeQuoteOut(client, provider, input) {
|
|
387
|
+
const { quoteMint, amount, user } = input;
|
|
388
|
+
if (amount <= 0n) throw new Error("the quote amount to sell must be positive");
|
|
389
|
+
if (quoteMint === "So11111111111111111111111111111111111111112") throw new Error("a WSOL-quoted pool needs no route: the builders unwrap SOL themselves");
|
|
390
|
+
return await provider.exactIn(client, quoteMint, WSOL_MINT, amount, user);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Refuse a route that would touch Gabox state, or that does not settle in the account the program
|
|
394
|
+
* binds.
|
|
395
|
+
*
|
|
396
|
+
* `forbidden` is every Gabox account the transaction itself uses, plus the Gabox program id.
|
|
397
|
+
* `settlesIn` is the user's quote associated token account: the swap has to name it, because that
|
|
398
|
+
* is where `buy_pack` measures the quote it spends and where `sell_tokens` measures the proceeds.
|
|
399
|
+
*/
|
|
400
|
+
function assertRouteIsSafe(route, expect) {
|
|
401
|
+
const forbidden = new Set(expect.forbidden);
|
|
402
|
+
let settles = false;
|
|
403
|
+
for (const instruction of route.instructions) {
|
|
404
|
+
if (forbidden.has(instruction.programAddress)) throw new Error(`the route calls ${instruction.programAddress}, which is a Gabox program or account. A route must never touch Gabox state.`);
|
|
405
|
+
for (const account of instruction.accounts ?? []) {
|
|
406
|
+
if (forbidden.has(account.address)) throw new Error(`the route names the Gabox account ${account.address}. A route must never touch Gabox state.`);
|
|
407
|
+
if (account.address === expect.settlesIn) settles = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!settles) throw new Error(`the route never names ${expect.settlesIn}, the quote account the program settles in. The swap would pay somewhere the buy cannot spend from.`);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* What `amount` of a quote token costs in SOL, through the client's route provider.
|
|
414
|
+
*
|
|
415
|
+
* `null` when the client has no provider, or when the provider has no exact-out route. A price is a
|
|
416
|
+
* display, so a missing one is not an error. A WSOL amount is already SOL and comes back unchanged.
|
|
417
|
+
*
|
|
418
|
+
* No fallback to exact-in here on purpose: an exact-in price answers a different question, and a
|
|
419
|
+
* display that silently swapped the two would be wrong rather than missing.
|
|
420
|
+
*/
|
|
421
|
+
async function solPriceOf(client, quoteMint, amount) {
|
|
422
|
+
if (quoteMint === "So11111111111111111111111111111111111111112") return amount;
|
|
423
|
+
if (!client.route || amount <= 0n) return null;
|
|
424
|
+
try {
|
|
425
|
+
return (await client.route.exactOut(client, WSOL_MINT, quoteMint, amount, quoteMint)).inAmount;
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/** `ceil(numerator / denominator)` for non-negative values. */
|
|
431
|
+
function ceilDiv(numerator, denominator) {
|
|
432
|
+
return (numerator + denominator - 1n) / denominator;
|
|
433
|
+
}
|
|
434
|
+
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
934
435
|
//#endregion
|
|
935
436
|
//#region src/offer.ts
|
|
936
437
|
/**
|
|
937
|
-
* The full offer for one machine.
|
|
438
|
+
* The full offer for one machine. Three round trips: the pool and its vault, the venue, then the
|
|
439
|
+
* quote mint and its metadata. A non-SOL pool adds one HTTP call to the route provider for
|
|
440
|
+
* `solAmount`.
|
|
441
|
+
*
|
|
442
|
+
* Throws when the coin has no pool.
|
|
443
|
+
*/
|
|
444
|
+
async function getOffer(client, mint, options = {}) {
|
|
445
|
+
const inventory = await fetchPoolInventory(client, mint);
|
|
446
|
+
if (!inventory) throw new Error(`no gabox pool for mint ${mint}`);
|
|
447
|
+
const { pool } = inventory;
|
|
448
|
+
const venue = await resolveVenue(client, {
|
|
449
|
+
mint,
|
|
450
|
+
user: options.user ?? pool.creator,
|
|
451
|
+
quote: {
|
|
452
|
+
mint: pool.quoteMint,
|
|
453
|
+
config: pool.quoteConfig,
|
|
454
|
+
tokenProgram: pool.quoteTokenProgram
|
|
455
|
+
},
|
|
456
|
+
...options.venue ? { venue: options.venue } : {}
|
|
457
|
+
});
|
|
458
|
+
const quoteAmount = venue.quoteBuy(pool.packTokens);
|
|
459
|
+
const display = await fetchQuoteDisplay(client, pool.quoteMint);
|
|
460
|
+
return offerFromState(inventory, venue.kind, quoteAmount, {
|
|
461
|
+
quoteDecimals: display.decimals,
|
|
462
|
+
quoteSymbol: display.symbol,
|
|
463
|
+
solAmount: await solPriceOf(client, pool.quoteMint, quoteAmount)
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The same computation with the reads already done. Useful when a caller holds a `ResolvedVenue`
|
|
468
|
+
* and wants to re-price without touching the network. `quoteAmount` is
|
|
469
|
+
* `venue.quoteBuy(pool.packTokens)`.
|
|
470
|
+
*
|
|
471
|
+
* `display` is optional: a caller that only wants the prize numbers can leave it out, and the three
|
|
472
|
+
* display fields then report the quote's own base units with no symbol and no SOL price.
|
|
473
|
+
*/
|
|
474
|
+
function offerFromState(inventory, venue, quoteAmount, display = {
|
|
475
|
+
quoteDecimals: 0,
|
|
476
|
+
quoteSymbol: null,
|
|
477
|
+
solAmount: null
|
|
478
|
+
}) {
|
|
479
|
+
const { pool } = inventory;
|
|
480
|
+
const tiers = tiersOf(pool);
|
|
481
|
+
const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
|
|
482
|
+
const uncapped = uncappedMaximum(pool.packTokens, tiers);
|
|
483
|
+
return {
|
|
484
|
+
mint: pool.mint,
|
|
485
|
+
pool: inventory.poolAddress,
|
|
486
|
+
packTokens: pool.packTokens,
|
|
487
|
+
quoteAmount,
|
|
488
|
+
quoteMint: pool.quoteMint,
|
|
489
|
+
quoteDecimals: display.quoteDecimals,
|
|
490
|
+
quoteSymbol: display.quoteSymbol,
|
|
491
|
+
solAmount: display.solAmount,
|
|
492
|
+
seedQuoteAmount: pool.seedQuoteAmount,
|
|
493
|
+
seedTokens: seedTokens(pool.packTokens, tiers),
|
|
494
|
+
venue,
|
|
495
|
+
prizes: offer.prizes,
|
|
496
|
+
maximum: offer.maximum,
|
|
497
|
+
minimum: offer.minimum,
|
|
498
|
+
uncapped,
|
|
499
|
+
inventory: inventory.inventory,
|
|
500
|
+
reserved: inventory.reserved,
|
|
501
|
+
free: inventory.free,
|
|
502
|
+
isFirstPack: pool.nextSeq === 0n,
|
|
503
|
+
isSeeded: offer.maximum === uncapped,
|
|
504
|
+
maxMultiplierBps: maxMultiplierBps(tiers),
|
|
505
|
+
averageMultiplierBps: averageMultiplierBps(tiers)
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* How short of the top prize a pool is, in tokens. `0` when it pays the whole table.
|
|
510
|
+
*
|
|
511
|
+
* The pack brings its own `packTokens` into the vault before the offer is computed, so the vault
|
|
512
|
+
* only has to hold `uncapped - packTokens` beforehand. Anything already reserved by another draw
|
|
513
|
+
* does not count. A donation of this size through `fund_prizes` uncaps the top prize again.
|
|
514
|
+
*/
|
|
515
|
+
function seedShortfall(offer) {
|
|
516
|
+
const needed = offer.uncapped > offer.packTokens ? offer.uncapped - offer.packTokens : 0n;
|
|
517
|
+
return offer.free >= needed ? 0n : needed - offer.free;
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/route/cpmm.ts
|
|
521
|
+
/**
|
|
522
|
+
* A route provider backed by one Raydium CPMM pool.
|
|
523
|
+
*
|
|
524
|
+
* Jupiter does not serve devnet, so a devnet machine quoted in a test token needs a route this SDK
|
|
525
|
+
* can build itself. Given a CPMM pool that holds the SOL/quote pair, this provider swaps through
|
|
526
|
+
* it with the same two instructions Gabox already forwards for a graduated coin, priced with the
|
|
527
|
+
* same bigint port of Raydium's math.
|
|
528
|
+
*
|
|
529
|
+
* It works anywhere such a pool exists, mainnet included. It is not a router: it uses the one pool
|
|
530
|
+
* the caller names and nothing else.
|
|
531
|
+
*
|
|
532
|
+
* # Wrapping
|
|
533
|
+
*
|
|
534
|
+
* CPMM settles in WSOL, never in native SOL. So the route wraps the SOL it spends and closes the
|
|
535
|
+
* WSOL account afterwards, exactly as Jupiter's `wrapAndUnwrapSol` does. A sale into SOL creates
|
|
536
|
+
* the WSOL account, swaps into it, and closes it, which is what turns the proceeds into SOL.
|
|
537
|
+
*
|
|
538
|
+
* # No signer objects
|
|
539
|
+
*
|
|
540
|
+
* The user is an address, not a signer. Every signing slot is marked as a signer and left for the
|
|
541
|
+
* fee payer to sign, the same way a Jupiter instruction arrives. The wallet paying for the Gabox
|
|
542
|
+
* transaction is the same wallet, so its one signature covers all of them.
|
|
543
|
+
*
|
|
544
|
+
* That is not a shortcut, it is required. Kit refuses to sign a message that carries two distinct
|
|
545
|
+
* signer objects for one address: `signTransactionMessageWithSigners` fails with "Multiple distinct
|
|
546
|
+
* signers were identified for address". The token and system builders below only accept a signer,
|
|
547
|
+
* so `withoutSigners` strips the object again and keeps the role.
|
|
548
|
+
*/
|
|
549
|
+
const u64 = getU64Encoder();
|
|
550
|
+
/**
|
|
551
|
+
* The slippage this provider signs for on a pool swap, in basis points. 1%, the same as the
|
|
552
|
+
* Jupiter provider's default. It only widens the on-chain bound; the price itself is exact.
|
|
553
|
+
*/
|
|
554
|
+
const CPMM_ROUTE_SLIPPAGE_BPS = 100n;
|
|
555
|
+
/**
|
|
556
|
+
* The compute units one swap through this provider adds to a transaction.
|
|
557
|
+
*
|
|
558
|
+
* Measured on devnet on 2026-09-18 against one Raydium CPMM pool, as the difference from the same
|
|
559
|
+
* builder with no route: `55,110` and `29,272` on `createMachine`, `21,488` and `52,980` on
|
|
560
|
+
* `buyPack`, and `30,138` and `51,138` on `sellTokens`. The spread is wide because which token
|
|
561
|
+
* accounts already exist changes from run to run, so this rounds up to the top of it.
|
|
562
|
+
*
|
|
563
|
+
* It is a safe figure here and nowhere else: this provider always uses exactly one pool. A router
|
|
564
|
+
* that may pick several hops states its own number; see `JUPITER_DEFAULT_COMPUTE_UNITS`.
|
|
565
|
+
*/
|
|
566
|
+
const CPMM_ROUTE_COMPUTE_UNITS = 75e3;
|
|
567
|
+
/**
|
|
568
|
+
* Swap through one named Raydium CPMM pool.
|
|
569
|
+
*
|
|
570
|
+
* The pool must hold the pair the route asks for. On devnet the SOL/USDC-test pool with the most
|
|
571
|
+
* liquidity is `5Eu2G2USTy1pqphmQzQ2SBXWrBq5sdhgEh7hso9R2xix`, under the fee tier
|
|
572
|
+
* `A9qBhPy4k5UYW72hSgAkh1Epr2do69P54yzzcMV3yv6b`.
|
|
573
|
+
*/
|
|
574
|
+
function raydiumCpmmRoute(poolAddress) {
|
|
575
|
+
return {
|
|
576
|
+
exactOut: async (client, input, output, amount, user) => await swap(client, poolAddress, {
|
|
577
|
+
input,
|
|
578
|
+
output,
|
|
579
|
+
amount,
|
|
580
|
+
user,
|
|
581
|
+
mode: "exactOut"
|
|
582
|
+
}),
|
|
583
|
+
exactIn: async (client, input, output, amount, user) => await swap(client, poolAddress, {
|
|
584
|
+
input,
|
|
585
|
+
output,
|
|
586
|
+
amount,
|
|
587
|
+
user,
|
|
588
|
+
mode: "exactIn"
|
|
589
|
+
})
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
async function swap(client, poolAddress, request) {
|
|
593
|
+
const { input, output, amount, user, mode } = request;
|
|
594
|
+
if (amount <= 0n) throw new Error("the route amount must be positive");
|
|
595
|
+
const ids = raydiumIds(client.cluster);
|
|
596
|
+
const [poolAccount] = await readAccounts(client.rpc, [poolAddress]);
|
|
597
|
+
if (!poolAccount || poolAccount.owner !== ids.cpmm) throw new Error(`${poolAddress} is not a Raydium CPMM pool on ${client.cluster}`);
|
|
598
|
+
const pool = decodeCpmmPool(poolAccount.data);
|
|
599
|
+
const inputIsToken0 = pool.token0Mint === input;
|
|
600
|
+
if (!(inputIsToken0 ? pool.token1Mint === output : pool.token1Mint === input && pool.token0Mint === output)) throw new Error(`the CPMM pool at ${poolAddress} holds ${pool.token0Mint} and ${pool.token1Mint}, not ${input} and ${output}`);
|
|
601
|
+
const inputVault = inputIsToken0 ? pool.token0Vault : pool.token1Vault;
|
|
602
|
+
const outputVault = inputIsToken0 ? pool.token1Vault : pool.token0Vault;
|
|
603
|
+
const inputTokenProgram = inputIsToken0 ? pool.token0Program : pool.token1Program;
|
|
604
|
+
const outputTokenProgram = inputIsToken0 ? pool.token1Program : pool.token0Program;
|
|
605
|
+
const [configAccount, inputVaultAccount, outputVaultAccount] = await readAccounts(client.rpc, [
|
|
606
|
+
pool.ammConfig,
|
|
607
|
+
inputVault,
|
|
608
|
+
outputVault
|
|
609
|
+
]);
|
|
610
|
+
if (!configAccount) throw new Error(`the CPMM pool at ${poolAddress} names a fee tier that is not on chain`);
|
|
611
|
+
if (!inputVaultAccount || !outputVaultAccount) throw new Error(`the CPMM pool at ${poolAddress} has no reserve accounts`);
|
|
612
|
+
const config = decodeCpmmAmmConfig(configAccount.data);
|
|
613
|
+
const owed = (token0) => token0 ? pool.protocolFeesToken0 + pool.fundFeesToken0 + pool.creatorFeesToken0 : pool.protocolFeesToken1 + pool.fundFeesToken1 + pool.creatorFeesToken1;
|
|
614
|
+
const sides = {
|
|
615
|
+
inputReserve: tokenAccountAmount(inputVaultAccount.data) - owed(inputIsToken0),
|
|
616
|
+
outputReserve: tokenAccountAmount(outputVaultAccount.data) - owed(!inputIsToken0)
|
|
617
|
+
};
|
|
618
|
+
if (sides.inputReserve <= 0n || sides.outputReserve <= 0n) throw new Error(`the CPMM pool at ${poolAddress} has no tradable reserves`);
|
|
619
|
+
const rates = {
|
|
620
|
+
tradeFeeRate: config.tradeFeeRate,
|
|
621
|
+
creatorFeeRate: pool.enableCreatorFee ? config.creatorFeeRate : 0n,
|
|
622
|
+
creatorFeeOnInput: creatorFeeOnInput(pool, input)
|
|
623
|
+
};
|
|
624
|
+
const userInput = await ata(user, input, inputTokenProgram);
|
|
625
|
+
const userOutput = await ata(user, output, outputTokenProgram);
|
|
626
|
+
const abi = mode === "exactOut" ? CPMM_SWAP_BASE_OUTPUT : CPMM_SWAP_BASE_INPUT;
|
|
627
|
+
const accounts = order(abi, {
|
|
628
|
+
payer: user,
|
|
629
|
+
authority: ids.cpmmAuthority,
|
|
630
|
+
amm_config: pool.ammConfig,
|
|
631
|
+
pool_state: poolAddress,
|
|
632
|
+
input_token_account: userInput,
|
|
633
|
+
output_token_account: userOutput,
|
|
634
|
+
input_vault: inputVault,
|
|
635
|
+
output_vault: outputVault,
|
|
636
|
+
input_token_program: inputTokenProgram,
|
|
637
|
+
output_token_program: outputTokenProgram,
|
|
638
|
+
input_token_mint: input,
|
|
639
|
+
output_token_mint: output,
|
|
640
|
+
observation_state: pool.observationKey
|
|
641
|
+
});
|
|
642
|
+
const exactOut = mode === "exactOut";
|
|
643
|
+
const priced = exactOut ? cpmmSwapBaseOutput(sides, rates, amount) : cpmmSwapBaseInput(sides, rates, amount);
|
|
644
|
+
const inAmount = exactOut ? widen(priced) : amount;
|
|
645
|
+
const outAmount = exactOut ? amount : narrow(priced);
|
|
646
|
+
return {
|
|
647
|
+
instructions: wrap({
|
|
648
|
+
user,
|
|
649
|
+
input,
|
|
650
|
+
output,
|
|
651
|
+
userInput,
|
|
652
|
+
userOutput,
|
|
653
|
+
inputTokenProgram,
|
|
654
|
+
outputTokenProgram,
|
|
655
|
+
lamportsIn: inAmount,
|
|
656
|
+
middle: [{
|
|
657
|
+
programAddress: ids.cpmm,
|
|
658
|
+
accounts,
|
|
659
|
+
data: new Uint8Array([
|
|
660
|
+
...abi.discriminator,
|
|
661
|
+
...u64.encode(exactOut ? inAmount : amount),
|
|
662
|
+
...u64.encode(exactOut ? amount : outAmount)
|
|
663
|
+
])
|
|
664
|
+
}]
|
|
665
|
+
}),
|
|
666
|
+
lookupTables: {},
|
|
667
|
+
inAmount,
|
|
668
|
+
outAmount,
|
|
669
|
+
mode,
|
|
670
|
+
computeUnits: CPMM_ROUTE_COMPUTE_UNITS
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/** Add the slippage margin to a cost the caller signs as a maximum. */
|
|
674
|
+
const widen = (amount) => amount + amount * CPMM_ROUTE_SLIPPAGE_BPS / 10000n;
|
|
675
|
+
/** Take the slippage margin off a payout the caller signs as a minimum. */
|
|
676
|
+
const narrow = (amount) => amount - amount * CPMM_ROUTE_SLIPPAGE_BPS / 10000n;
|
|
677
|
+
/**
|
|
678
|
+
* Create the two token accounts the swap needs, wrap the SOL it spends, and close the WSOL account
|
|
679
|
+
* afterwards.
|
|
680
|
+
*
|
|
681
|
+
* Only one side is ever WSOL here: a Gabox pool quoted in WSOL never uses a route at all.
|
|
682
|
+
*/
|
|
683
|
+
function wrap(input) {
|
|
684
|
+
const payer = createNoopSigner(input.user);
|
|
685
|
+
const createAta = (account, mint, tokenProgram) => withoutSigners(getCreateAssociatedTokenIdempotentInstruction({
|
|
686
|
+
payer,
|
|
687
|
+
ata: account,
|
|
688
|
+
owner: input.user,
|
|
689
|
+
mint,
|
|
690
|
+
tokenProgram
|
|
691
|
+
}));
|
|
692
|
+
const before = [createAta(input.userInput, input.input, input.inputTokenProgram), createAta(input.userOutput, input.output, input.outputTokenProgram)];
|
|
693
|
+
const after = [];
|
|
694
|
+
if (input.input === "So11111111111111111111111111111111111111112") {
|
|
695
|
+
before.push(withoutSigners(getTransferSolInstruction({
|
|
696
|
+
source: payer,
|
|
697
|
+
destination: input.userInput,
|
|
698
|
+
amount: input.lamportsIn
|
|
699
|
+
})), getSyncNativeInstruction({ account: input.userInput }));
|
|
700
|
+
after.push(closeWsol(input.userInput, payer));
|
|
701
|
+
}
|
|
702
|
+
if (input.output === "So11111111111111111111111111111111111111112") after.push(closeWsol(input.userOutput, payer));
|
|
703
|
+
return [
|
|
704
|
+
...before,
|
|
705
|
+
...input.middle,
|
|
706
|
+
...after
|
|
707
|
+
];
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Close a WSOL account, sending every lamport in it back to the owner as SOL.
|
|
711
|
+
*
|
|
712
|
+
* The owner has to sign, so it goes in as a signer and comes out as a plain signing slot.
|
|
713
|
+
*/
|
|
714
|
+
const closeWsol = (account, owner) => withoutSigners(getCloseAccountInstruction({
|
|
715
|
+
account,
|
|
716
|
+
destination: owner.address,
|
|
717
|
+
owner
|
|
718
|
+
}));
|
|
719
|
+
/**
|
|
720
|
+
* Drop every attached signer object, keeping each account's address and role.
|
|
721
|
+
*
|
|
722
|
+
* A signing slot stays a signing slot: the compiled message still requires that signature, and the
|
|
723
|
+
* wallet paying for the transaction provides it. What goes away is the second signer object for an
|
|
724
|
+
* address the fee payer already covers, which kit refuses to sign.
|
|
725
|
+
*/
|
|
726
|
+
function withoutSigners(instruction) {
|
|
727
|
+
const accounts = (instruction.accounts ?? []).map((account) => ({
|
|
728
|
+
address: account.address,
|
|
729
|
+
role: account.role
|
|
730
|
+
}));
|
|
731
|
+
return {
|
|
732
|
+
...instruction,
|
|
733
|
+
accounts
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/route/jupiter.ts
|
|
738
|
+
/**
|
|
739
|
+
* The Jupiter route provider.
|
|
740
|
+
*
|
|
741
|
+
* Jupiter is an HTTP service, not a program this SDK builds instructions for. Two calls per route:
|
|
742
|
+
*
|
|
743
|
+
* 1. `GET /swap/v1/quote` prices the swap and returns a quote object.
|
|
744
|
+
* 2. `POST /swap/v1/swap-instructions` turns that quote into instructions.
|
|
745
|
+
*
|
|
746
|
+
* The response gives `setupInstructions`, `swapInstruction` and `cleanupInstruction`, each as a
|
|
747
|
+
* program id, a list of accounts and base64 data. This module decodes those into kit instructions
|
|
748
|
+
* and leaves everything else alone. Jupiter's own compute budget instructions are dropped: every
|
|
749
|
+
* builder in this SDK sets its own budget, and two `SetComputeUnitLimit` instructions in one message
|
|
750
|
+
* is one too many. Their **number** is kept, though: it is Jupiter's own answer to "how much does
|
|
751
|
+
* this route cost", and a builder adds it to its own limit.
|
|
752
|
+
*
|
|
753
|
+
* `wrapAndUnwrapSol: true` is always sent, so Jupiter creates the wallet's WSOL account, funds it
|
|
754
|
+
* from the wallet's lamports and closes it again inside its own instructions. That is what makes
|
|
755
|
+
* "pay in SOL" true from the wallet's side.
|
|
756
|
+
*
|
|
757
|
+
* # No Jupiter package
|
|
758
|
+
*
|
|
759
|
+
* The public surface of this SDK is `@solana/kit` only. Nothing here imports a Jupiter package; the
|
|
760
|
+
* response is plain JSON and the decoding below is a dozen lines.
|
|
761
|
+
*
|
|
762
|
+
* # Exact-out is not always available
|
|
763
|
+
*
|
|
764
|
+
* Jupiter answers `NO_ROUTES_FOUND` for an exact-out quote whenever the best route has more than
|
|
765
|
+
* one hop. Verified on 2026-09-18: SOL to USDC quotes exact-out, while SOL to the stock token
|
|
766
|
+
* `XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB` only quotes exact-in. `routeQuoteIn` in `leg.ts`
|
|
767
|
+
* handles that fallback; this file only reports the failure.
|
|
768
|
+
*/
|
|
769
|
+
/** Jupiter's free endpoint. The keyed host `https://api.jup.ag/swap/v1` has the same shape. */
|
|
770
|
+
const JUPITER_LITE_URL = "https://lite-api.jup.ag/swap/v1";
|
|
771
|
+
/** The slippage Jupiter prices a route with when the caller names none. 1%. */
|
|
772
|
+
const JUPITER_DEFAULT_SLIPPAGE_BPS = 100;
|
|
773
|
+
/**
|
|
774
|
+
* The compute units a Jupiter route is assumed to need when the response carries no limit.
|
|
938
775
|
*
|
|
939
|
-
*
|
|
776
|
+
* Jupiter normally sends a `SetComputeUnitLimit` of its own, and that number is what this SDK uses.
|
|
777
|
+
* When it does not, this is the fallback: enough for a route through several pools, and still far
|
|
778
|
+
* below the 1,400,000-unit ceiling once the Gabox instruction's own budget is added. A caller who
|
|
779
|
+
* knows better passes `computeUnitLimit` to the builder.
|
|
940
780
|
*/
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
781
|
+
const JUPITER_DEFAULT_COMPUTE_UNITS = 4e5;
|
|
782
|
+
/** `ComputeBudgetInstruction::SetComputeUnitLimit`, whose data is the tag then a u32 of units. */
|
|
783
|
+
const SET_COMPUTE_UNIT_LIMIT = 2;
|
|
784
|
+
const base64 = getBase64Encoder();
|
|
785
|
+
/**
|
|
786
|
+
* A route provider backed by Jupiter. Use it on mainnet, where Jupiter has the liquidity.
|
|
787
|
+
*
|
|
788
|
+
* It makes read-only HTTP calls and never sends a transaction: the instructions come back to the
|
|
789
|
+
* caller, who signs them together with the Gabox instruction.
|
|
790
|
+
*/
|
|
791
|
+
function jupiterRoute(options = {}) {
|
|
792
|
+
const url = (options.url ?? "https://lite-api.jup.ag/swap/v1").replace(/\/+$/, "");
|
|
793
|
+
const slippageBps = options.slippageBps ?? 100;
|
|
794
|
+
const build = async (client, input, output, amount, user, swapMode) => {
|
|
795
|
+
if (amount <= 0n) throw new Error("the route amount must be positive");
|
|
796
|
+
const quote = await fetchQuote(url, {
|
|
797
|
+
input,
|
|
798
|
+
output,
|
|
799
|
+
amount,
|
|
800
|
+
swapMode,
|
|
801
|
+
slippageBps
|
|
802
|
+
});
|
|
803
|
+
const response = await fetchSwapInstructions(url, quote, user);
|
|
804
|
+
const threshold = BigInt(String(quote.otherAmountThreshold));
|
|
805
|
+
return await routeFrom(client, response, {
|
|
806
|
+
inAmount: swapMode === "ExactOut" ? threshold : BigInt(String(quote.inAmount)),
|
|
807
|
+
outAmount: swapMode === "ExactOut" ? BigInt(String(quote.outAmount)) : threshold,
|
|
808
|
+
mode: swapMode === "ExactOut" ? "exactOut" : "exactIn"
|
|
809
|
+
});
|
|
810
|
+
};
|
|
811
|
+
return {
|
|
812
|
+
exactOut: async (client, input, output, amount, user) => await build(client, input, output, amount, user, "ExactOut"),
|
|
813
|
+
exactIn: async (client, input, output, amount, user) => await build(client, input, output, amount, user, "ExactIn")
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async function fetchQuote(url, input) {
|
|
817
|
+
const query = new URLSearchParams({
|
|
818
|
+
inputMint: input.input,
|
|
819
|
+
outputMint: input.output,
|
|
820
|
+
amount: input.amount.toString(),
|
|
821
|
+
swapMode: input.swapMode,
|
|
822
|
+
slippageBps: String(input.slippageBps)
|
|
948
823
|
});
|
|
949
|
-
|
|
824
|
+
const response = await fetch(`${url}/quote?${query.toString()}`);
|
|
825
|
+
const body = await response.json();
|
|
826
|
+
if (!response.ok || body.error) throw new Error(`Jupiter has no ${input.swapMode} route from ${input.input} to ${input.output}: ` + `${body.errorCode ?? response.status} ${body.error ?? ""}`.trim());
|
|
827
|
+
return body;
|
|
828
|
+
}
|
|
829
|
+
async function fetchSwapInstructions(url, quoteResponse, userPublicKey) {
|
|
830
|
+
const response = await fetch(`${url}/swap-instructions`, {
|
|
831
|
+
method: "POST",
|
|
832
|
+
headers: { "Content-Type": "application/json" },
|
|
833
|
+
body: JSON.stringify({
|
|
834
|
+
quoteResponse,
|
|
835
|
+
userPublicKey,
|
|
836
|
+
wrapAndUnwrapSol: true
|
|
837
|
+
})
|
|
838
|
+
});
|
|
839
|
+
const body = await response.json();
|
|
840
|
+
if (!response.ok || body.error || !body.swapInstruction) throw new Error(`Jupiter could not build the swap instructions: ${response.status} ${body.error ?? ""}`.trim());
|
|
841
|
+
return body;
|
|
950
842
|
}
|
|
951
843
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
844
|
+
* Turn a decoded `swap-instructions` response into a `Route`.
|
|
845
|
+
*
|
|
846
|
+
* Exported so a test can read a recorded response without making an HTTP call. The lookup tables
|
|
847
|
+
* are read through the client, because the response names them by address only.
|
|
955
848
|
*/
|
|
956
|
-
function
|
|
957
|
-
const { pool } = inventory;
|
|
958
|
-
const tiers = tiersOf(pool);
|
|
959
|
-
const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
|
|
960
|
-
const uncapped = uncappedMaximum(pool.packTokens, tiers);
|
|
849
|
+
async function routeFrom(client, response, amounts) {
|
|
961
850
|
return {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
seedTokens: pool.seedTokens,
|
|
971
|
-
venue,
|
|
972
|
-
prizes: offer.prizes,
|
|
973
|
-
maximum: offer.maximum,
|
|
974
|
-
minimum: offer.minimum,
|
|
975
|
-
uncapped,
|
|
976
|
-
inventory: inventory.inventory,
|
|
977
|
-
reserved: inventory.reserved,
|
|
978
|
-
free: inventory.free,
|
|
979
|
-
isFirstPack: pool.nextSeq === 0n,
|
|
980
|
-
isSeeded: offer.maximum === uncapped,
|
|
981
|
-
maxMultiplierBps: maxMultiplierBps(tiers),
|
|
982
|
-
averageMultiplierBps: averageMultiplierBps(tiers)
|
|
851
|
+
instructions: [
|
|
852
|
+
...(response.setupInstructions ?? []).map(toKitInstruction),
|
|
853
|
+
toKitInstruction(response.swapInstruction),
|
|
854
|
+
...response.cleanupInstruction ? [toKitInstruction(response.cleanupInstruction)] : []
|
|
855
|
+
],
|
|
856
|
+
lookupTables: await fetchAddressLookupTables(client, response.addressLookupTableAddresses ?? []),
|
|
857
|
+
computeUnits: computeUnitsOf(response),
|
|
858
|
+
...amounts
|
|
983
859
|
};
|
|
984
860
|
}
|
|
985
861
|
/**
|
|
986
|
-
*
|
|
862
|
+
* The unit limit Jupiter asked for, or `JUPITER_DEFAULT_COMPUTE_UNITS` when it asked for none.
|
|
987
863
|
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
* does not count. A donation of this size through `fund_prizes` uncaps the top prize again.
|
|
864
|
+
* `SetComputeUnitLimit` is five bytes: the tag `2`, then the units as a little-endian u32. Any other
|
|
865
|
+
* compute budget instruction, such as a unit price, is skipped.
|
|
991
866
|
*/
|
|
992
|
-
function
|
|
993
|
-
const
|
|
994
|
-
|
|
867
|
+
function computeUnitsOf(response) {
|
|
868
|
+
for (const instruction of response.computeBudgetInstructions ?? []) {
|
|
869
|
+
const data = new Uint8Array(base64.encode(instruction.data));
|
|
870
|
+
if (data.length < 5 || data[0] !== SET_COMPUTE_UNIT_LIMIT) continue;
|
|
871
|
+
return new DataView(data.buffer, data.byteOffset).getUint32(1, true);
|
|
872
|
+
}
|
|
873
|
+
return JUPITER_DEFAULT_COMPUTE_UNITS;
|
|
874
|
+
}
|
|
875
|
+
/** One Jupiter instruction as a kit instruction. The roles come from the two booleans. */
|
|
876
|
+
function toKitInstruction(instruction) {
|
|
877
|
+
const accounts = instruction.accounts.map((account) => ({
|
|
878
|
+
address: account.pubkey,
|
|
879
|
+
role: account.isSigner ? account.isWritable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER : account.isWritable ? AccountRole.WRITABLE : AccountRole.READONLY
|
|
880
|
+
}));
|
|
881
|
+
return {
|
|
882
|
+
programAddress: instruction.programId,
|
|
883
|
+
accounts,
|
|
884
|
+
data: new Uint8Array(base64.encode(instruction.data))
|
|
885
|
+
};
|
|
995
886
|
}
|
|
996
887
|
//#endregion
|
|
997
888
|
//#region src/rpc.ts
|
|
@@ -1054,8 +945,8 @@ function clusterNamedBy(url) {
|
|
|
1054
945
|
*/
|
|
1055
946
|
function assertClusterUrl(cluster, url) {
|
|
1056
947
|
const named = clusterNamedBy(url);
|
|
1057
|
-
if (cluster === "devnet" && named !== "devnet") throw new Error(`refusing to use ${url} as a devnet endpoint: it does not name devnet.\
|
|
1058
|
-
if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\
|
|
948
|
+
if (cluster === "devnet" && named !== "devnet") throw new Error(`refusing to use ${url} as a devnet endpoint: it does not name devnet.\nA Gabox program id, a MagicBlock queue and a pool address all exist on every cluster, so a wrong URL is a live transaction, not an error. Pass { cluster: 'localnet' } for a local validator, or name the cluster the URL really is.`);
|
|
949
|
+
if (cluster !== "devnet" && named !== null && named !== cluster) throw new Error(`refusing to use ${url} as a ${cluster} endpoint: the URL names ${named}.\nA Gabox address exists on every cluster, so a wrong URL is a live transaction, not an error. Pass the cluster the URL really names.`);
|
|
1059
950
|
}
|
|
1060
951
|
/** `https://x` becomes `wss://x`, `http://x` becomes `ws://x`. Anything else is returned as is. */
|
|
1061
952
|
function websocketUrlFor(url) {
|
|
@@ -1083,311 +974,451 @@ function createClient(config) {
|
|
|
1083
974
|
wsUrl,
|
|
1084
975
|
rpc: createSolanaRpc(url),
|
|
1085
976
|
rpcSubscriptions: createSolanaRpcSubscriptions(wsUrl),
|
|
1086
|
-
addressLookupTables: config.addressLookupTables ?? defaultAddressLookupTables(cluster)
|
|
977
|
+
addressLookupTables: config.addressLookupTables ?? defaultAddressLookupTables(cluster),
|
|
978
|
+
route: config.route === void 0 ? defaultRoute(cluster) : config.route
|
|
1087
979
|
};
|
|
1088
980
|
}
|
|
1089
|
-
//#endregion
|
|
1090
|
-
//#region src/tx/message.ts
|
|
1091
981
|
/**
|
|
1092
|
-
*
|
|
982
|
+
* The swap provider a cluster gets when the caller names none.
|
|
1093
983
|
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
* nothing in this SDK signs or sends anything itself.
|
|
984
|
+
* Jupiter on mainnet, nothing anywhere else. Jupiter's API only prices mainnet liquidity, and a
|
|
985
|
+
* devnet caller has to say which pool to route through, so there is nothing to guess.
|
|
1097
986
|
*/
|
|
987
|
+
function defaultRoute(cluster) {
|
|
988
|
+
return cluster === "mainnet-beta" ? jupiterRoute() : null;
|
|
989
|
+
}
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/tx/wsol.ts
|
|
1098
992
|
/**
|
|
1099
|
-
*
|
|
993
|
+
* Wrapping and unwrapping SOL around a venue trade.
|
|
994
|
+
*
|
|
995
|
+
* Both venues settle in WSOL, never in native SOL. So every builder in this directory does the same
|
|
996
|
+
* three things around its Gabox instruction:
|
|
1100
997
|
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
998
|
+
* 1. create the wallet's WSOL associated token account, if it is missing;
|
|
999
|
+
* 2. move the lamports it is going to spend into that account and `syncNative` it, so the token
|
|
1000
|
+
* balance matches the lamports;
|
|
1001
|
+
* 3. close the account afterwards, which sends everything left back to the wallet as SOL.
|
|
1002
|
+
*
|
|
1003
|
+
* A sale needs no step 2: the proceeds arrive in the account, and the close is what turns them into
|
|
1004
|
+
* SOL.
|
|
1005
|
+
*
|
|
1006
|
+
* # Closing unwraps everything
|
|
1007
|
+
*
|
|
1008
|
+
* If the wallet already held WSOL in that account, the close turns that into SOL too. Nothing is
|
|
1009
|
+
* lost and the wallet still owns every lamport, but the balance moves out of the token account. A
|
|
1010
|
+
* wallet that keeps a WSOL position on purpose should build its own instructions instead of using
|
|
1011
|
+
* these builders.
|
|
1103
1012
|
*/
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1013
|
+
/** The wallet's WSOL account, and the instructions that put `lamports` of spendable WSOL in it. */
|
|
1014
|
+
async function fundWsol(owner, lamports) {
|
|
1015
|
+
if (lamports < 0n) throw new Error("lamports must not be negative");
|
|
1016
|
+
const account = await wsolAccountFor(owner.address);
|
|
1017
|
+
const instructions = [getCreateAssociatedTokenIdempotentInstruction({
|
|
1018
|
+
payer: owner,
|
|
1019
|
+
ata: account,
|
|
1020
|
+
owner: owner.address,
|
|
1021
|
+
mint: WSOL_MINT,
|
|
1022
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
1023
|
+
})];
|
|
1024
|
+
if (lamports > 0n) instructions.push(getTransferSolInstruction({
|
|
1025
|
+
source: owner,
|
|
1026
|
+
destination: account,
|
|
1027
|
+
amount: lamports
|
|
1028
|
+
}), getSyncNativeInstruction({ account }));
|
|
1114
1029
|
return {
|
|
1115
|
-
|
|
1116
|
-
|
|
1030
|
+
account,
|
|
1031
|
+
instructions
|
|
1117
1032
|
};
|
|
1118
1033
|
}
|
|
1034
|
+
/** Close the WSOL account, sending every lamport in it back to the owner as SOL. */
|
|
1035
|
+
function unwrapWsol(owner, account) {
|
|
1036
|
+
return getCloseAccountInstruction({
|
|
1037
|
+
account,
|
|
1038
|
+
destination: owner.address,
|
|
1039
|
+
owner
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1119
1042
|
//#endregion
|
|
1120
|
-
//#region src/
|
|
1121
|
-
/**
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1043
|
+
//#region src/tx/quoteLeg.ts
|
|
1044
|
+
/**
|
|
1045
|
+
* Getting the quote token into, and out of, the wallet's own quote account.
|
|
1046
|
+
*
|
|
1047
|
+
* Gabox settles in the pool's quote asset and nothing else. Both venues move that token in and out
|
|
1048
|
+
* of one account: the wallet's associated token account for the quote mint, under the quote's own
|
|
1049
|
+
* token program. The program pins that address and measures the exact delta there. So every builder
|
|
1050
|
+
* in this directory has to make sure the account exists, and holds what the trade will spend.
|
|
1051
|
+
*
|
|
1052
|
+
* Three shapes, and which one applies follows from the pool's quote and the caller's choice:
|
|
1053
|
+
*
|
|
1054
|
+
* - **A WSOL pool.** The wallet pays in SOL already. Create the WSOL account, move the lamports
|
|
1055
|
+
* into it, `syncNative`, and close it afterwards so the change and any proceeds come back as
|
|
1056
|
+
* SOL. This is what every 0.6.0 flow did, unchanged.
|
|
1057
|
+
* - **Another quote, paying in that quote.** The wallet already holds the token. Create the
|
|
1058
|
+
* account if it is missing and leave it alone: it is not WSOL, so closing it would be wrong.
|
|
1059
|
+
* - **Another quote, paying in SOL.** A route turns SOL into the quote token in the same
|
|
1060
|
+
* transaction, before `buy_pack`. A sale does the reverse afterwards. `routeQuoteIn` and
|
|
1061
|
+
* `routeQuoteOut` build those, and `assertRouteIsSafe` checks the result before it is used.
|
|
1062
|
+
*
|
|
1063
|
+
* Nothing here ever splits the work across two transactions. A swap that settles separately would
|
|
1064
|
+
* leave the wallet holding a token it never asked for whenever the second half failed.
|
|
1065
|
+
*/
|
|
1066
|
+
/** Create the wallet's quote account if it is missing. Idempotent, so a second create is free. */
|
|
1067
|
+
function createQuoteAccount(owner, venue) {
|
|
1068
|
+
return getCreateAssociatedTokenIdempotentInstruction({
|
|
1069
|
+
payer: owner,
|
|
1070
|
+
ata: venue.userQuoteToken,
|
|
1071
|
+
owner: owner.address,
|
|
1072
|
+
mint: venue.quoteMint,
|
|
1073
|
+
tokenProgram: venue.quoteTokenProgram
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* The leg that puts `maxQuoteIn` of the quote token in the buyer's quote account.
|
|
1078
|
+
*
|
|
1079
|
+
* `payWith` decides where it comes from. On a WSOL pool the choice makes no difference: the quote
|
|
1080
|
+
* token is SOL either way, so the builder wraps it.
|
|
1081
|
+
*/
|
|
1082
|
+
async function quoteLegIn(client, input) {
|
|
1083
|
+
const { venue, payer, maxQuoteIn, payWith } = input;
|
|
1084
|
+
if (venue.quoteMint === "So11111111111111111111111111111111111111112") {
|
|
1085
|
+
const wsol = await fundWsol(payer, maxQuoteIn);
|
|
1086
|
+
return {
|
|
1087
|
+
before: wsol.instructions,
|
|
1088
|
+
after: [unwrapWsol(payer, wsol.account)],
|
|
1089
|
+
lookupTables: {},
|
|
1090
|
+
mode: null,
|
|
1091
|
+
solAmount: maxQuoteIn,
|
|
1092
|
+
computeUnits: 0
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (payWith === "quote") return {
|
|
1096
|
+
before: [createQuoteAccount(payer, venue)],
|
|
1097
|
+
after: [],
|
|
1098
|
+
lookupTables: {},
|
|
1099
|
+
mode: null,
|
|
1100
|
+
solAmount: null,
|
|
1101
|
+
computeUnits: 0
|
|
1102
|
+
};
|
|
1103
|
+
const route = await routeQuoteIn(client, providerOf(client, venue.quoteMint), {
|
|
1104
|
+
quoteMint: venue.quoteMint,
|
|
1105
|
+
amount: maxQuoteIn,
|
|
1106
|
+
user: payer.address
|
|
1107
|
+
});
|
|
1108
|
+
assertRouteIsSafe(route, {
|
|
1109
|
+
forbidden: [GABOX_PROGRAM_ID, ...input.gaboxAccounts],
|
|
1110
|
+
settlesIn: venue.userQuoteToken
|
|
1111
|
+
});
|
|
1127
1112
|
return {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1113
|
+
before: route.instructions,
|
|
1114
|
+
after: [],
|
|
1115
|
+
lookupTables: route.lookupTables,
|
|
1116
|
+
mode: route.mode,
|
|
1117
|
+
solAmount: route.inAmount,
|
|
1118
|
+
computeUnits: route.computeUnits
|
|
1131
1119
|
};
|
|
1132
1120
|
}
|
|
1133
|
-
/**
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* The leg around a sale: make sure the quote account exists, and turn the proceeds into SOL when
|
|
1123
|
+
* the seller asked for SOL.
|
|
1124
|
+
*
|
|
1125
|
+
* The swap is an exact-in of `minQuoteOutput`, the floor the seller already signs for on the sale
|
|
1126
|
+
* itself. Anything the venue pays above that floor stays in the seller's quote account: a swap can
|
|
1127
|
+
* only spend what the sale is guaranteed to have produced.
|
|
1128
|
+
*/
|
|
1129
|
+
async function quoteLegOut(client, input) {
|
|
1130
|
+
const { venue, seller, minQuoteOutput, receive } = input;
|
|
1131
|
+
if (venue.quoteMint === "So11111111111111111111111111111111111111112") {
|
|
1132
|
+
const wsol = await fundWsol(seller, 0n);
|
|
1133
|
+
return {
|
|
1134
|
+
before: wsol.instructions,
|
|
1135
|
+
after: [unwrapWsol(seller, wsol.account)],
|
|
1136
|
+
lookupTables: {},
|
|
1137
|
+
mode: null,
|
|
1138
|
+
solAmount: null,
|
|
1139
|
+
computeUnits: 0
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
const create = createQuoteAccount(seller, venue);
|
|
1143
|
+
if (receive === "quote") return {
|
|
1144
|
+
before: [create],
|
|
1145
|
+
after: [],
|
|
1146
|
+
lookupTables: {},
|
|
1147
|
+
mode: null,
|
|
1148
|
+
solAmount: null,
|
|
1149
|
+
computeUnits: 0
|
|
1150
|
+
};
|
|
1151
|
+
const route = await routeQuoteOut(client, providerOf(client, venue.quoteMint), {
|
|
1152
|
+
quoteMint: venue.quoteMint,
|
|
1153
|
+
amount: minQuoteOutput,
|
|
1154
|
+
user: seller.address
|
|
1142
1155
|
});
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
return await buildMessage(client, referrer, [await getClaimReferralInstructionAsync({
|
|
1147
|
-
referrer,
|
|
1148
|
-
pool
|
|
1149
|
-
})], {
|
|
1150
|
-
addressLookupTables: options.addressLookupTables,
|
|
1151
|
-
computeUnitLimit: options.computeUnitLimit ?? 8e4,
|
|
1152
|
-
...options.computeUnitPrice === void 0 ? {} : { computeUnitPrice: options.computeUnitPrice }
|
|
1156
|
+
assertRouteIsSafe(route, {
|
|
1157
|
+
forbidden: [GABOX_PROGRAM_ID, ...input.gaboxAccounts],
|
|
1158
|
+
settlesIn: venue.userQuoteToken
|
|
1153
1159
|
});
|
|
1160
|
+
return {
|
|
1161
|
+
before: [create],
|
|
1162
|
+
after: route.instructions,
|
|
1163
|
+
lookupTables: route.lookupTables,
|
|
1164
|
+
mode: route.mode,
|
|
1165
|
+
solAmount: route.outAmount,
|
|
1166
|
+
computeUnits: route.computeUnits
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/** The client's route provider, with a message that says what to do when it has none. */
|
|
1170
|
+
function providerOf(client, quoteMint) {
|
|
1171
|
+
if (!client.route) throw new Error(`this ${client.cluster} client has no route provider, so it cannot pay in SOL for a pool quoted in ${quoteMint}. Pass \`route\` to createClient — raydiumCpmmRoute(pool) for a CPMM pool that holds the SOL pair — or pay in the quote token itself.`);
|
|
1172
|
+
return client.route;
|
|
1154
1173
|
}
|
|
1155
|
-
//#endregion
|
|
1156
|
-
//#region src/tx/buyPack.ts
|
|
1157
1174
|
/**
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* One instruction does the whole thing: buy exactly `pool.packTokens` at the venue, move the
|
|
1161
|
-
* tokens into gabox custody, freeze a prize table into a new `Draw`, pay the creator's fee and
|
|
1162
|
-
* the protocol's 1% of the venue cost, and ask MagicBlock for randomness. Either all of it happens
|
|
1163
|
-
* or none of it does.
|
|
1175
|
+
* The compute limit a builder asks for: its own budget plus whatever the route needs.
|
|
1164
1176
|
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
*
|
|
1174
|
-
* `maxTotalDebit` caps the venue debit plus the creator fee plus the protocol fee plus the VRF
|
|
1175
|
-
* request, measured in SOL and WSOL together. Both fees are a share of the venue debit, so budget
|
|
1176
|
-
* them from `maxQuoteIn`. It does **not** cover the rent Anchor pays for the `Draw` and any ATA it
|
|
1177
|
-
* creates, nor the transaction fee: the runtime charges those before the handler runs. Budget for
|
|
1178
|
-
* them separately.
|
|
1179
|
-
*
|
|
1180
|
-
* # PumpSwap needs WSOL first
|
|
1177
|
+
* Capped at the runtime's ceiling. Jupiter often asks for the whole 1,400,000 units rather than
|
|
1178
|
+
* estimating, and a request above the ceiling is rejected outright, so the sum has to be clamped
|
|
1179
|
+
* rather than passed through.
|
|
1180
|
+
*/
|
|
1181
|
+
function computeUnitsWithRoute(own, leg) {
|
|
1182
|
+
return Math.min(own + leg.computeUnits, MAX_COMPUTE_UNIT_LIMIT);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Add the route to a "transaction is too large" error.
|
|
1181
1186
|
*
|
|
1182
|
-
*
|
|
1183
|
-
* the
|
|
1184
|
-
*
|
|
1185
|
-
* instructions, signed by the buyer, and gabox never touches that account.
|
|
1187
|
+
* `buildMessage` already refuses a message above the 1,232-byte limit. When a swap is in the same
|
|
1188
|
+
* message, the reason is usually the swap, and the fix is not to split the transaction: the two
|
|
1189
|
+
* halves have to settle together. So the message says what a caller can actually do instead.
|
|
1186
1190
|
*/
|
|
1191
|
+
function routeSizeHint(cause, leg) {
|
|
1192
|
+
if (leg.mode === null) return cause;
|
|
1193
|
+
if (!(cause instanceof Error) || !cause.message.includes("Solana allows 1232")) return cause;
|
|
1194
|
+
return new Error(`${cause.message} The swap and the Gabox instruction share one transaction on purpose, so this SDK never splits them. Supply more address lookup tables, or pay in the quote token.`, { cause });
|
|
1195
|
+
}
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/tx/buyPack.ts
|
|
1187
1198
|
async function buyPack(client, input) {
|
|
1188
|
-
const { mint, purchaser } = input;
|
|
1189
1199
|
if (input.maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
1190
|
-
|
|
1191
|
-
|
|
1200
|
+
if (input.maxNativeDebit < 0n) throw new Error("maxNativeDebit must not be negative");
|
|
1201
|
+
const inventory = await fetchPoolInventory(client, input.mint);
|
|
1202
|
+
if (!inventory) throw new Error(`no Gabox pool for mint ${input.mint}`);
|
|
1192
1203
|
const { pool, poolAddress } = inventory;
|
|
1204
|
+
const purchaser = input.purchaser.address;
|
|
1193
1205
|
const venue = await resolveVenue(client, {
|
|
1194
|
-
mint,
|
|
1195
|
-
user: purchaser
|
|
1206
|
+
mint: input.mint,
|
|
1207
|
+
user: purchaser,
|
|
1208
|
+
quote: {
|
|
1209
|
+
mint: pool.quoteMint,
|
|
1210
|
+
config: pool.quoteConfig,
|
|
1211
|
+
tokenProgram: pool.quoteTokenProgram
|
|
1212
|
+
},
|
|
1196
1213
|
...input.venue ? { venue: input.venue } : {}
|
|
1197
1214
|
});
|
|
1198
1215
|
const draw = await drawAddress(poolAddress, input.seq ?? pool.nextSeq);
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if (referral === null && input.referrer && canBind(purchaser.address, input.referrer)) {
|
|
1202
|
-
const referrer = input.referrer;
|
|
1203
|
-
bind = await getBindReferrerInstructionAsync({
|
|
1204
|
-
referee: purchaser,
|
|
1205
|
-
referrer
|
|
1206
|
-
});
|
|
1207
|
-
referral = {
|
|
1208
|
-
link: await referralLinkAddress(purchaser.address),
|
|
1209
|
-
referrer,
|
|
1210
|
-
referral: await referralAddress(poolAddress, referrer)
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
const buy = getBuyPackInstruction({
|
|
1214
|
-
purchaser,
|
|
1216
|
+
const buy = await getBuyPackInstructionAsync({
|
|
1217
|
+
purchaser: input.purchaser,
|
|
1215
1218
|
pool: poolAddress,
|
|
1216
|
-
creator: pool.creator,
|
|
1217
1219
|
draw,
|
|
1218
|
-
mint,
|
|
1220
|
+
mint: input.mint,
|
|
1221
|
+
quoteMint: pool.quoteMint,
|
|
1219
1222
|
vault: pool.vault,
|
|
1220
|
-
userTokens: venueUserTokens(venue.buyAccounts, venue.kind),
|
|
1221
1223
|
venue: venue.program,
|
|
1222
|
-
|
|
1223
|
-
tokenProgram: pool.tokenProgram,
|
|
1224
|
+
quoteTokenProgram: pool.quoteTokenProgram,
|
|
1224
1225
|
maxQuoteIn: input.maxQuoteIn,
|
|
1225
1226
|
minMaximum: input.minMaximum,
|
|
1226
|
-
|
|
1227
|
-
referralLink: referral?.link,
|
|
1228
|
-
referral: referral?.referral
|
|
1227
|
+
maxNativeDebit: input.maxNativeDebit
|
|
1229
1228
|
});
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1229
|
+
const leg = await quoteLegIn(client, {
|
|
1230
|
+
venue,
|
|
1231
|
+
payer: input.purchaser,
|
|
1232
|
+
maxQuoteIn: input.maxQuoteIn,
|
|
1233
|
+
payWith: input.payWith ?? "sol",
|
|
1234
|
+
gaboxAccounts: [
|
|
1235
|
+
poolAddress,
|
|
1236
|
+
pool.vault,
|
|
1237
|
+
draw,
|
|
1238
|
+
await activityAddress(purchaser),
|
|
1239
|
+
await associatedTokenAddress(purchaser, input.mint)
|
|
1240
|
+
]
|
|
1239
1241
|
});
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
* The program refuses a self-referral and a link to the all-zero address. Both would take the
|
|
1245
|
-
* pack down with them, so the bind is skipped instead and the pack goes through unreferred.
|
|
1246
|
-
*/
|
|
1247
|
-
function canBind(purchaser, referrer) {
|
|
1248
|
-
return referrer !== purchaser && referrer !== NO_REFERRER;
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* The purchaser's base-token ATA, taken out of the venue account list rather than re-derived.
|
|
1252
|
-
*
|
|
1253
|
-
* `market.rs` requires `user_tokens` to equal the venue list's own `associated_base_user` (Pump) or
|
|
1254
|
-
* `user_base_token_account` (PumpSwap). Reading it back from the list makes the two agree by
|
|
1255
|
-
* construction instead of by two derivations that happen to match.
|
|
1256
|
-
*/
|
|
1257
|
-
function venueUserTokens(accounts, kind) {
|
|
1258
|
-
const account = accounts[kind === "pump" ? 14 : 5];
|
|
1259
|
-
if (!account) throw new Error(`the ${kind} account list is too short`);
|
|
1260
|
-
return account.address;
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* Create the buyer's WSOL account if needed, fund it, and sync its balance.
|
|
1264
|
-
*
|
|
1265
|
-
* `SyncNative` is the step people forget. A plain SOL transfer into a WSOL account raises its
|
|
1266
|
-
* lamports but not the `amount` field the token program reads, so the venue sees an empty account
|
|
1267
|
-
* until this instruction copies one to the other.
|
|
1268
|
-
*/
|
|
1269
|
-
function wsolPreparation$2(purchaser, lamports) {
|
|
1270
|
-
const wsol = wsolAccountFor(purchaser.address);
|
|
1271
|
-
return [
|
|
1272
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1273
|
-
payer: purchaser,
|
|
1274
|
-
ata: wsol,
|
|
1275
|
-
owner: purchaser.address,
|
|
1276
|
-
mint: WSOL_MINT,
|
|
1277
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1278
|
-
}),
|
|
1279
|
-
getTransferSolInstruction({
|
|
1280
|
-
source: purchaser,
|
|
1281
|
-
destination: wsol,
|
|
1282
|
-
amount: lamports
|
|
1283
|
-
}),
|
|
1284
|
-
getSyncNativeInstruction({ account: wsol })
|
|
1242
|
+
const instructions = [
|
|
1243
|
+
...leg.before,
|
|
1244
|
+
withRemainingAccounts(buy, venue.buyAccounts),
|
|
1245
|
+
...leg.after
|
|
1285
1246
|
];
|
|
1247
|
+
try {
|
|
1248
|
+
return await buildMessage(client, input.purchaser, instructions, {
|
|
1249
|
+
addressLookupTables: {
|
|
1250
|
+
...input.addressLookupTables ?? client.addressLookupTables,
|
|
1251
|
+
...leg.lookupTables
|
|
1252
|
+
},
|
|
1253
|
+
computeUnitLimit: input.computeUnitLimit ?? computeUnitsWithRoute(235e3, leg),
|
|
1254
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1255
|
+
});
|
|
1256
|
+
} catch (cause) {
|
|
1257
|
+
throw routeSizeHint(cause, leg);
|
|
1258
|
+
}
|
|
1286
1259
|
}
|
|
1287
1260
|
//#endregion
|
|
1288
1261
|
//#region src/tx/createMachine.ts
|
|
1289
1262
|
/**
|
|
1290
|
-
* Creating a machine: one transaction, two instructions, two signers.
|
|
1291
|
-
*
|
|
1292
|
-
* # Why it has to be one transaction
|
|
1293
|
-
*
|
|
1294
|
-
* `initialize_pool` reads the Instructions sysvar and refuses to run unless the same transaction
|
|
1295
|
-
* also carries a Pump `create_v2` for the same mint, signed by the same creator. That is what makes
|
|
1296
|
-
* "one pool per coin" true and stops anyone wrapping an existing coin in a machine. The create must
|
|
1297
|
-
* come **first**: the mint account has to exist and deserialize before Anchor validates
|
|
1298
|
-
* `initialize_pool`'s accounts.
|
|
1299
|
-
*
|
|
1300
|
-
* # The seed
|
|
1301
|
-
*
|
|
1302
|
-
* The creator owns none of the coin yet — it does not exist until this transaction runs. So
|
|
1303
|
-
* `initialize_pool` buys the seed on the curve itself, into the creator's own ATA, and moves it
|
|
1304
|
-
* straight into the vault. The Pump buy accounts ride along as `remainingAccounts`, in the same
|
|
1305
|
-
* order `buy_pack` uses.
|
|
1306
|
-
*
|
|
1307
|
-
* The seed follows from the tier table. The program accepts any table that passes `math::validate`;
|
|
1308
|
-
* it does not enforce one table. A creator may pass their own `tiers`; the default is
|
|
1309
|
-
* `DEFAULT_TIERS`. The program buys exactly `largestTierAmount - PACK_TOKENS` tokens as the seed,
|
|
1310
|
-
* so the first pack can pay the top tier in full. The creator only signs a maximum SOL cost for
|
|
1311
|
-
* that buy. The seed buy moves the curve, so a bigger jackpot means a slightly higher starting
|
|
1312
|
-
* pack price. A fresh Pump curve only holds so many tokens (`Global.initialRealTokenReserves`),
|
|
1313
|
-
* so a table whose seed exceeds that cannot be created; `createMachine` and `seedCostEstimate`
|
|
1314
|
-
* both check this before spending anything.
|
|
1315
|
-
*
|
|
1316
|
-
* # Two signers
|
|
1317
|
-
*
|
|
1318
|
-
* The mint keypair signs `create_v2` — Pump takes it as a signer rather than deriving it — and the
|
|
1319
|
-
* creator signs both instructions and pays for everything.
|
|
1320
|
-
*/
|
|
1321
|
-
/**
|
|
1322
1263
|
* Build the transaction message. Sign it with both `creator` and `mintKeypair`.
|
|
1323
1264
|
*
|
|
1324
|
-
* Reads
|
|
1325
|
-
*
|
|
1265
|
+
* Reads the quote's LaunchLab config, the quote mint, and the Gabox platform config, because the
|
|
1266
|
+
* seed price and every quote-side account depend on them. Nothing else needs the chain: the coin
|
|
1326
1267
|
* does not exist yet, so every other account is a derivation.
|
|
1327
1268
|
*/
|
|
1328
1269
|
async function createMachine(client, input) {
|
|
1329
|
-
const { creator, mintKeypair, name, symbol, uri,
|
|
1330
|
-
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 100) throw new Error(`feeBps must be a whole number from 0 to 100`);
|
|
1270
|
+
const { creator, mintKeypair, name, symbol, uri, maxSeedQuoteIn, maxSeedNativeDebit } = input;
|
|
1331
1271
|
const tiers = cloneTiers(input.tiers ?? DEFAULT_TIERS);
|
|
1332
1272
|
validateTiers(tiers);
|
|
1333
1273
|
validatePack(PACK_TOKENS, tiers);
|
|
1334
|
-
const
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1274
|
+
const extraSeedTokens = input.extraSeedTokens ?? 0n;
|
|
1275
|
+
if (extraSeedTokens < 0n) throw new Error("extraSeedTokens must not be negative");
|
|
1276
|
+
const seed = seedTokens(PACK_TOKENS, tiers) + extraSeedTokens;
|
|
1277
|
+
assertSeedFitsCurve(seed);
|
|
1278
|
+
if (seed > 0n && maxSeedQuoteIn <= 0n) throw new Error("maxSeedQuoteIn must be positive when the jackpot needs a seed");
|
|
1279
|
+
if (maxSeedQuoteIn < 0n) throw new Error("maxSeedQuoteIn must not be negative");
|
|
1280
|
+
if (maxSeedNativeDebit < 0n) throw new Error("maxSeedNativeDebit must not be negative");
|
|
1281
|
+
const ids = raydiumIds(client.cluster);
|
|
1337
1282
|
const mint = mintKeypair.address;
|
|
1338
|
-
const
|
|
1283
|
+
const quote = await fetchQuoteAsset(client, input.quote?.mint ?? "So11111111111111111111111111111111111111112", ids);
|
|
1284
|
+
const raise = resolveRaise(client, quote, input.raise, ids);
|
|
1285
|
+
const create = await getLaunchInstruction({
|
|
1339
1286
|
mint: mintKeypair,
|
|
1340
|
-
|
|
1287
|
+
creator,
|
|
1341
1288
|
name,
|
|
1342
1289
|
symbol,
|
|
1343
1290
|
uri,
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1291
|
+
quoteMint: quote.mint,
|
|
1292
|
+
quoteConfig: quote.config,
|
|
1293
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1294
|
+
raise
|
|
1295
|
+
}, ids);
|
|
1296
|
+
const poolState = await launchlabPoolAddress(ids.launchlab, mint, quote.mint);
|
|
1297
|
+
const userQuoteToken = await quoteAccountFor(creator.address, quote.mint, quote.tokenProgram);
|
|
1298
|
+
const venueAccounts = launchlabBuyAccounts({
|
|
1299
|
+
launchlab: ids.launchlab,
|
|
1300
|
+
launchlabAuthority: ids.launchlabAuthority,
|
|
1301
|
+
launchlabEventAuthority: ids.launchlabEventAuthority,
|
|
1302
|
+
globalConfig: quote.config,
|
|
1303
|
+
platformConfig: ids.gaboxPlatform,
|
|
1304
|
+
poolState,
|
|
1349
1305
|
mint,
|
|
1306
|
+
quoteMint: quote.mint,
|
|
1307
|
+
baseVault: await launchlabVaultAddress(ids.launchlab, poolState, mint),
|
|
1308
|
+
quoteVault: await launchlabVaultAddress(ids.launchlab, poolState, quote.mint),
|
|
1350
1309
|
user: creator.address,
|
|
1351
|
-
|
|
1352
|
-
|
|
1310
|
+
userBaseToken: await ata(creator.address, mint),
|
|
1311
|
+
userQuoteToken,
|
|
1312
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1313
|
+
platformFeeVault: await platformFeeVaultAddress(ids.launchlab, ids.gaboxPlatform, quote.mint),
|
|
1314
|
+
creatorFeeVault: await creatorFeeVaultAddress(ids.launchlab, creator.address, quote.mint)
|
|
1353
1315
|
});
|
|
1354
|
-
|
|
1316
|
+
const initialize = await getInitializePoolInstructionAsync({
|
|
1355
1317
|
creator,
|
|
1356
1318
|
mint,
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1319
|
+
quoteMint: quote.mint,
|
|
1320
|
+
quoteConfig: quote.config,
|
|
1321
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1322
|
+
venue: ids.launchlab,
|
|
1360
1323
|
tiers,
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1324
|
+
maxSeedQuoteIn,
|
|
1325
|
+
maxSeedNativeDebit,
|
|
1326
|
+
extraSeedTokens
|
|
1327
|
+
});
|
|
1328
|
+
const leg = await quoteLegIn(client, {
|
|
1329
|
+
venue: {
|
|
1330
|
+
quoteMint: quote.mint,
|
|
1331
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1332
|
+
userQuoteToken
|
|
1333
|
+
},
|
|
1334
|
+
payer: creator,
|
|
1335
|
+
maxQuoteIn: maxSeedQuoteIn,
|
|
1336
|
+
payWith: seed === 0n ? "quote" : input.payWith ?? "sol",
|
|
1337
|
+
gaboxAccounts: [
|
|
1338
|
+
await poolAddress(mint),
|
|
1339
|
+
await vaultAddress(mint),
|
|
1340
|
+
await ata(creator.address, mint)
|
|
1341
|
+
]
|
|
1366
1342
|
});
|
|
1343
|
+
const instructions = [
|
|
1344
|
+
create,
|
|
1345
|
+
...leg.before,
|
|
1346
|
+
withRemainingAccounts(initialize, venueAccounts),
|
|
1347
|
+
...leg.after
|
|
1348
|
+
];
|
|
1349
|
+
try {
|
|
1350
|
+
return await buildMessage(client, creator, instructions, {
|
|
1351
|
+
addressLookupTables: {
|
|
1352
|
+
...input.addressLookupTables ?? client.addressLookupTables,
|
|
1353
|
+
...leg.lookupTables
|
|
1354
|
+
},
|
|
1355
|
+
computeUnitLimit: input.computeUnitLimit ?? computeUnitsWithRoute(35e4, leg),
|
|
1356
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1357
|
+
});
|
|
1358
|
+
} catch (cause) {
|
|
1359
|
+
throw routeSizeHint(cause, leg);
|
|
1360
|
+
}
|
|
1367
1361
|
}
|
|
1368
1362
|
/**
|
|
1369
1363
|
* What the seed for this table costs, fees included, and how many tokens it is.
|
|
1370
1364
|
*
|
|
1371
|
-
* The coin does not exist yet, so the price
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1365
|
+
* The coin does not exist yet, so the price comes from the starting reserves LaunchLab derives from
|
|
1366
|
+
* the launch shape and the raise. Nothing trades on the curve before `initialize_pool` runs in the
|
|
1367
|
+
* same transaction, so this is exact up to a change in Raydium's fee rates between the read and the
|
|
1368
|
+
* send.
|
|
1369
|
+
*
|
|
1370
|
+
* `solAmount` is the same cost in SOL, priced through the client's route provider. It is `null`
|
|
1371
|
+
* when the client has no provider, or when no route exists: a devnet client has none unless the
|
|
1372
|
+
* caller passes `raydiumCpmmRoute(pool)`.
|
|
1374
1373
|
*
|
|
1375
|
-
* Defaults to `DEFAULT_TIERS
|
|
1376
|
-
*
|
|
1374
|
+
* Defaults to `DEFAULT_TIERS`, wrapped SOL and no extra seed. Throws if `tiers` fails
|
|
1375
|
+
* `validateTiers`/`validatePack`, if `extraSeedTokens` is negative, if the total seed is bigger
|
|
1376
|
+
* than the curve sells, or if the raise is missing or below what LaunchLab accepts.
|
|
1377
1377
|
*/
|
|
1378
|
-
async function seedCostEstimate(client, tiers = DEFAULT_TIERS) {
|
|
1378
|
+
async function seedCostEstimate(client, tiers = DEFAULT_TIERS, options = {}) {
|
|
1379
1379
|
const copiedTiers = cloneTiers(tiers);
|
|
1380
1380
|
validateTiers(copiedTiers);
|
|
1381
1381
|
validatePack(PACK_TOKENS, copiedTiers);
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1382
|
+
const extraSeedTokens = options.extraSeedTokens ?? 0n;
|
|
1383
|
+
if (extraSeedTokens < 0n) throw new Error("extraSeedTokens must not be negative");
|
|
1384
|
+
const mandatorySeed = seedTokens(PACK_TOKENS, copiedTiers);
|
|
1385
|
+
const seed = mandatorySeed + extraSeedTokens;
|
|
1386
|
+
assertSeedFitsCurve(seed);
|
|
1387
|
+
const ids = raydiumIds(client.cluster);
|
|
1388
|
+
const quote = await fetchQuoteAsset(client, options.quote?.mint ?? "So11111111111111111111111111111111111111112", ids);
|
|
1389
|
+
const raise = resolveRaise(client, quote, options.raise, ids);
|
|
1390
|
+
const settings = await fetchCurveSettings(client, quote.config, ids);
|
|
1391
|
+
const quoteAmount = seed === 0n ? 0n : curveBuyExactOut(newCurveReserves(raise, settings.migrateFee), settings.rates, seed);
|
|
1384
1392
|
return {
|
|
1385
1393
|
tiers: copiedTiers,
|
|
1386
|
-
seedTokens:
|
|
1387
|
-
|
|
1394
|
+
seedTokens: mandatorySeed,
|
|
1395
|
+
extraSeedTokens,
|
|
1396
|
+
totalSeedTokens: seed,
|
|
1397
|
+
quoteAmount,
|
|
1398
|
+
quoteMint: quote.mint,
|
|
1399
|
+
quoteDecimals: quote.decimals,
|
|
1400
|
+
quoteSymbol: quote.symbol,
|
|
1401
|
+
raise,
|
|
1402
|
+
solAmount: await solPriceOf(client, quote.mint, quoteAmount)
|
|
1388
1403
|
};
|
|
1389
1404
|
}
|
|
1390
|
-
/**
|
|
1405
|
+
/**
|
|
1406
|
+
* The raise a launch uses, checked before anything is built.
|
|
1407
|
+
*
|
|
1408
|
+
* The program pins it for a WSOL pool, so a different value there is a launch that would be
|
|
1409
|
+
* refused on chain. Any other quote has no default and no pin: the caller names it, and LaunchLab's
|
|
1410
|
+
* own minimum for that quote is the floor.
|
|
1411
|
+
*/
|
|
1412
|
+
function resolveRaise(client, quote, raise, ids) {
|
|
1413
|
+
if (quote.mint === "So11111111111111111111111111111111111111112") {
|
|
1414
|
+
if (raise !== void 0 && raise !== ids.launchQuoteRaise) throw new Error(`a WSOL pool raises exactly ${ids.launchQuoteRaise} lamports on ${client.cluster}; the program refuses ${raise}. Leave \`raise\` out, or pick another quote asset.`);
|
|
1415
|
+
return ids.launchQuoteRaise;
|
|
1416
|
+
}
|
|
1417
|
+
if (raise === void 0) throw new Error(`a pool quoted in ${quote.mint} needs a \`raise\`, in that token's base units. There is no default, because the same number means a different amount in every token.`);
|
|
1418
|
+
if (raise < quote.minQuoteFundRaising) throw new Error(`LaunchLab takes at least ${quote.minQuoteFundRaising} base units of ${quote.mint} as a raise, and this launch asks for ${raise}`);
|
|
1419
|
+
return raise;
|
|
1420
|
+
}
|
|
1421
|
+
/** A mutable table shape, owned by this call and safe to pass to Codama's builder. */
|
|
1391
1422
|
function cloneTiers(tiers) {
|
|
1392
1423
|
return tiers.map(({ multiplierBps, tickets }) => ({
|
|
1393
1424
|
multiplierBps,
|
|
@@ -1395,67 +1426,25 @@ function cloneTiers(tiers) {
|
|
|
1395
1426
|
}));
|
|
1396
1427
|
}
|
|
1397
1428
|
/**
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
* seeded. This throws before any buy is attempted.
|
|
1402
|
-
*/
|
|
1403
|
-
async function assertSeedFitsFreshCurve(client, seed) {
|
|
1404
|
-
if (seed === 0n) return;
|
|
1405
|
-
const { value } = await client.rpc.getAccountInfo(PUMP_GLOBAL, {
|
|
1406
|
-
encoding: "base64",
|
|
1407
|
-
commitment: "confirmed"
|
|
1408
|
-
}).send();
|
|
1409
|
-
if (!value) throw new Error(`Pump's Global account is missing at ${PUMP_GLOBAL}`);
|
|
1410
|
-
const global = decodePumpGlobal(new Uint8Array(getBase64Encoder().encode(value.data[0])));
|
|
1411
|
-
if (seed > global.initialRealTokenReserves) throw new Error(`the seed (${seed} tokens) is bigger than a fresh Pump curve holds (${global.initialRealTokenReserves} tokens); this table's top tier cannot be seeded on a new coin`);
|
|
1412
|
-
}
|
|
1413
|
-
/**
|
|
1414
|
-
* The Pump buy accounts for the seed, built without reading the bonding curve.
|
|
1415
|
-
*
|
|
1416
|
-
* The curve does not exist yet — `create_v2` in the same transaction is what creates it — so
|
|
1417
|
-
* `resolveVenue` cannot be used here. Everything the account list needs is known anyway: the
|
|
1418
|
-
* creator vault follows from the `creator` argument that `create_v2` records on the curve, and the
|
|
1419
|
-
* two fee recipients come from `Global`.
|
|
1429
|
+
* The curve sells `LAUNCH_TOTAL_BASE_SELL` coins in total, so a seed above that cannot be bought at
|
|
1430
|
+
* any price. A buy for more than the curve has left is capped rather than refused, so it would look
|
|
1431
|
+
* cheap instead of failing. This throws before any transaction is built.
|
|
1420
1432
|
*/
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
encoding: "base64",
|
|
1424
|
-
commitment: "confirmed"
|
|
1425
|
-
}).send();
|
|
1426
|
-
if (!value) throw new Error(`Pump's Global account is missing at ${PUMP_GLOBAL}`);
|
|
1427
|
-
const global = decodePumpGlobal(new Uint8Array(getBase64Encoder().encode(value.data[0])));
|
|
1428
|
-
const recipients = [global.feeRecipient, ...global.feeRecipients];
|
|
1429
|
-
const feeRecipient = recipients[options.feeRecipientIndex ?? randomIndex(recipients.length)];
|
|
1430
|
-
const buyback = global.buybackFeeRecipients[options.buybackRecipientIndex ?? randomIndex(global.buybackFeeRecipients.length)];
|
|
1431
|
-
if (!feeRecipient) throw new Error("Pump Global holds no fee recipient");
|
|
1432
|
-
if (!buyback) throw new Error("Pump Global holds no buyback fee recipient");
|
|
1433
|
-
return pumpBuyAccounts({
|
|
1434
|
-
mint: options.mint,
|
|
1435
|
-
user: options.user,
|
|
1436
|
-
tokenProgram: PUMP_CREATE_TOKEN_PROGRAM_ADDRESS,
|
|
1437
|
-
creator: options.user,
|
|
1438
|
-
feeRecipient,
|
|
1439
|
-
buybackFeeRecipient: buyback
|
|
1440
|
-
});
|
|
1433
|
+
function assertSeedFitsCurve(seed) {
|
|
1434
|
+
if (seed > 793100000000000n) throw new Error(`the seed (${seed} base units) is bigger than the whole curve sells (${LAUNCH_TOTAL_BASE_SELL}); this table's top tier cannot be seeded on a new coin`);
|
|
1441
1435
|
}
|
|
1442
|
-
const randomIndex = (length) => {
|
|
1443
|
-
if (length === 0) throw new Error("the list is empty");
|
|
1444
|
-
return Math.floor(Math.random() * length);
|
|
1445
|
-
};
|
|
1446
1436
|
//#endregion
|
|
1447
1437
|
//#region src/tx/draw.ts
|
|
1448
|
-
/** A retry and an expiry are both small. The default budget is plenty. */
|
|
1449
1438
|
const DRAW_COMPUTE_UNITS = 2e5;
|
|
1450
1439
|
async function retryDraw(client, input) {
|
|
1451
|
-
const
|
|
1440
|
+
const ix = await getRetryDrawInstruction({
|
|
1452
1441
|
payer: input.payer,
|
|
1453
1442
|
pool: input.pool,
|
|
1454
1443
|
draw: input.draw,
|
|
1455
1444
|
identity: await vrfIdentityAddress(),
|
|
1456
1445
|
maxVrfDebit: input.maxVrfDebit
|
|
1457
1446
|
});
|
|
1458
|
-
return await buildMessage(client, input.payer, [
|
|
1447
|
+
return await buildMessage(client, input.payer, [ix], {
|
|
1459
1448
|
addressLookupTables: input.addressLookupTables,
|
|
1460
1449
|
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1461
1450
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
@@ -1463,294 +1452,117 @@ async function retryDraw(client, input) {
|
|
|
1463
1452
|
}
|
|
1464
1453
|
async function expireDraw(client, input) {
|
|
1465
1454
|
const draw = await fetchDraw(client, input.draw);
|
|
1466
|
-
if (!draw) throw new Error(
|
|
1467
|
-
if (draw.pool !== input.pool) throw new Error(`draw ${input.draw} does not belong to ${input.pool}`);
|
|
1455
|
+
if (!draw || draw.pool !== input.pool) throw new Error("draw does not belong to pool or was already delivered");
|
|
1468
1456
|
const pool = await fetchPoolAt(client, input.pool);
|
|
1469
1457
|
if (!pool) throw new Error(`no pool at ${input.pool}`);
|
|
1470
|
-
const
|
|
1458
|
+
const ix = getExpireDrawInstruction({
|
|
1471
1459
|
pool: input.pool,
|
|
1472
1460
|
draw: input.draw,
|
|
1473
1461
|
purchaser: draw.purchaser,
|
|
1474
1462
|
mint: pool.mint,
|
|
1475
1463
|
vault: pool.vault,
|
|
1476
|
-
userTokens: await associatedTokenAddress(draw.purchaser, pool.mint
|
|
1477
|
-
tokenProgram: pool.tokenProgram
|
|
1464
|
+
userTokens: await associatedTokenAddress(draw.purchaser, pool.mint)
|
|
1478
1465
|
});
|
|
1479
|
-
return await buildMessage(client, input.payer, [
|
|
1466
|
+
return await buildMessage(client, input.payer, [ix], {
|
|
1480
1467
|
addressLookupTables: input.addressLookupTables,
|
|
1481
1468
|
computeUnitLimit: input.computeUnitLimit ?? DRAW_COMPUTE_UNITS,
|
|
1482
1469
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1483
1470
|
});
|
|
1484
1471
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
*/
|
|
1492
|
-
async function drawAvailability(client, draw) {
|
|
1493
|
-
const record = await fetchDraw(client, draw);
|
|
1494
|
-
if (!record) return null;
|
|
1495
|
-
const slot = await client.rpc.getSlot({ commitment: "confirmed" }).send();
|
|
1496
|
-
const now = BigInt(slot);
|
|
1497
|
-
const retryAt = record.lastAttemptSlot + RETRY_SLOTS;
|
|
1498
|
-
const expireAt = record.requestSlot + TIMEOUT_SLOTS;
|
|
1472
|
+
async function drawAvailability(client, address) {
|
|
1473
|
+
const draw = await fetchDraw(client, address);
|
|
1474
|
+
if (!draw) return null;
|
|
1475
|
+
const now = BigInt(await client.rpc.getSlot({ commitment: "confirmed" }).send());
|
|
1476
|
+
const retryAt = draw.lastAttemptSlot + RETRY_SLOTS;
|
|
1477
|
+
const expireAt = draw.requestSlot + TIMEOUT_SLOTS;
|
|
1499
1478
|
const slotsUntilRetry = now >= retryAt ? 0n : retryAt - now;
|
|
1500
1479
|
const slotsUntilExpiry = now >= expireAt ? 0n : expireAt - now;
|
|
1501
|
-
const pending = record.status === 0;
|
|
1502
1480
|
return {
|
|
1503
|
-
|
|
1504
|
-
attempts: record.attempts,
|
|
1481
|
+
attempts: draw.attempts,
|
|
1505
1482
|
slotsUntilRetry,
|
|
1506
1483
|
slotsUntilExpiry,
|
|
1507
|
-
canRetry:
|
|
1508
|
-
canExpire:
|
|
1484
|
+
canRetry: draw.attempts < 3 && slotsUntilRetry === 0n && slotsUntilExpiry > 0n,
|
|
1485
|
+
canExpire: slotsUntilExpiry === 0n
|
|
1509
1486
|
};
|
|
1510
1487
|
}
|
|
1511
1488
|
//#endregion
|
|
1512
1489
|
//#region src/tx/fundPrizes.ts
|
|
1513
|
-
/**
|
|
1514
|
-
* Donating prize inventory.
|
|
1515
|
-
*
|
|
1516
|
-
* `fund_prizes` moves tokens from any wallet into the pool's vault. It is a donation and it is
|
|
1517
|
-
* irrevocable: there is no withdrawal instruction, and no authority can move vault tokens. The only
|
|
1518
|
-
* way out of the vault is prize delivery.
|
|
1519
|
-
*
|
|
1520
|
-
* Funding raises what later packs can pay. It does not change the tiers, which are immutable, and
|
|
1521
|
-
* it does not change any draw that is already frozen — `math.quote` is computed at purchase time
|
|
1522
|
-
* and stored in the `Draw`.
|
|
1523
|
-
*/
|
|
1524
1490
|
const FUND_COMPUTE_UNITS = 2e5;
|
|
1525
|
-
/** A venue buy plus the vault transfer. The buy is the expensive half. */
|
|
1526
|
-
const FUND_WITH_BUY_COMPUTE_UNITS = 4e5;
|
|
1527
1491
|
async function fundPrizes(client, input) {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
mint,
|
|
1536
|
-
source: input.source ?? await associatedTokenAddress(funder.address, mint, pool.tokenProgram),
|
|
1492
|
+
if (input.amount <= 0n) throw new Error("amount must be positive");
|
|
1493
|
+
const pool = await fetchPoolByMint(client, input.mint);
|
|
1494
|
+
if (!pool) throw new Error(`no Gabox pool for mint ${input.mint}`);
|
|
1495
|
+
const ix = getFundPrizesInstruction({
|
|
1496
|
+
funder: input.funder,
|
|
1497
|
+
pool: await poolAddress(input.mint),
|
|
1498
|
+
mint: input.mint,
|
|
1499
|
+
source: input.source ?? await associatedTokenAddress(input.funder.address, input.mint),
|
|
1537
1500
|
vault: pool.vault,
|
|
1538
|
-
|
|
1539
|
-
amount
|
|
1540
|
-
})], {
|
|
1541
|
-
addressLookupTables: input.addressLookupTables,
|
|
1542
|
-
computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
|
|
1543
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Buy tokens at the venue with SOL and donate them to the vault, in one transaction.
|
|
1548
|
-
*
|
|
1549
|
-
* This is what a donor with SOL and no coins needs. `fundPrizes` moves tokens the donor already
|
|
1550
|
-
* holds; this one buys them first. Both are irrevocable — there is no withdrawal instruction, and
|
|
1551
|
-
* no authority can move vault tokens.
|
|
1552
|
-
*
|
|
1553
|
-
* Three instructions on the curve route: create the donor's token account if it is missing, buy,
|
|
1554
|
-
* donate. Pump creates the account itself, but the idempotent instruction costs nothing when it
|
|
1555
|
-
* already exists and it makes the transaction correct on its own terms. The PumpSwap route adds the
|
|
1556
|
-
* WSOL create/fund/sync prefix, for the same reason `buyPack` does: PumpSwap spends WSOL.
|
|
1557
|
-
*/
|
|
1558
|
-
async function fundPrizesWithBuy(client, input) {
|
|
1559
|
-
const { mint, funder, tokens, maxQuoteIn } = input;
|
|
1560
|
-
if (tokens <= 0n) throw new Error("tokens must be positive");
|
|
1561
|
-
if (maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
1562
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1563
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1564
|
-
const venue = await resolveVenue(client, {
|
|
1565
|
-
mint,
|
|
1566
|
-
user: funder.address,
|
|
1567
|
-
...input.venue ? { venue: input.venue } : {}
|
|
1501
|
+
amount: input.amount
|
|
1568
1502
|
});
|
|
1569
|
-
|
|
1570
|
-
return await buildMessage(client, funder, [
|
|
1571
|
-
...venue.kind === "pumpswap" ? wsolPreparation$1(funder, input.wrapLamports ?? maxQuoteIn) : [],
|
|
1572
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1573
|
-
payer: funder,
|
|
1574
|
-
ata: source,
|
|
1575
|
-
owner: funder.address,
|
|
1576
|
-
mint,
|
|
1577
|
-
tokenProgram: pool.tokenProgram
|
|
1578
|
-
}),
|
|
1579
|
-
venueBuyInstruction({
|
|
1580
|
-
venue,
|
|
1581
|
-
user: funder,
|
|
1582
|
-
tokens,
|
|
1583
|
-
maxQuoteIn
|
|
1584
|
-
}),
|
|
1585
|
-
getFundPrizesInstruction({
|
|
1586
|
-
funder,
|
|
1587
|
-
pool: await poolAddress(mint),
|
|
1588
|
-
mint,
|
|
1589
|
-
source,
|
|
1590
|
-
vault: pool.vault,
|
|
1591
|
-
tokenProgram: pool.tokenProgram,
|
|
1592
|
-
amount: tokens
|
|
1593
|
-
})
|
|
1594
|
-
], {
|
|
1503
|
+
return await buildMessage(client, input.funder, [ix], {
|
|
1595
1504
|
addressLookupTables: input.addressLookupTables,
|
|
1596
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
1505
|
+
computeUnitLimit: input.computeUnitLimit ?? FUND_COMPUTE_UNITS,
|
|
1597
1506
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1598
1507
|
});
|
|
1599
1508
|
}
|
|
1600
|
-
/** Create the donor's WSOL account if needed, fund it, and sync its balance. See `buyPack.ts`. */
|
|
1601
|
-
function wsolPreparation$1(funder, lamports) {
|
|
1602
|
-
const wsol = wsolAccountFor(funder.address);
|
|
1603
|
-
return [
|
|
1604
|
-
getCreateAssociatedTokenIdempotentInstruction({
|
|
1605
|
-
payer: funder,
|
|
1606
|
-
ata: wsol,
|
|
1607
|
-
owner: funder.address,
|
|
1608
|
-
mint: WSOL_MINT,
|
|
1609
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1610
|
-
}),
|
|
1611
|
-
getTransferSolInstruction({
|
|
1612
|
-
source: funder,
|
|
1613
|
-
destination: wsol,
|
|
1614
|
-
amount: lamports
|
|
1615
|
-
}),
|
|
1616
|
-
getSyncNativeInstruction({ account: wsol })
|
|
1617
|
-
];
|
|
1618
|
-
}
|
|
1619
1509
|
//#endregion
|
|
1620
1510
|
//#region src/tx/redeem.ts
|
|
1621
|
-
/**
|
|
1622
|
-
* Selling an automatically delivered prize, plus settlement helpers for legacy resolve-only draws.
|
|
1623
|
-
*
|
|
1624
|
-
* Every sale here goes through the program (`sell_tokens` or `sell_prize`), which does the venue
|
|
1625
|
-
* CPI and then pays the protocol 1% of what the sale returned. On Pump the proceeds and the fee are
|
|
1626
|
-
* native SOL. On PumpSwap both are WSOL: the proceeds land in the seller's WSOL account and the fee
|
|
1627
|
-
* moves from there to the collector's WSOL account. Add an unwrap afterwards if the seller wants
|
|
1628
|
-
* lamports.
|
|
1629
|
-
*
|
|
1630
|
-
* `minQuoteOutput` is the venue's own floor, checked before the fee. The seller keeps 99% of
|
|
1631
|
-
* whatever the venue paid above it.
|
|
1632
|
-
*/
|
|
1633
|
-
/**
|
|
1634
|
-
* Sell a prize that the VRF callback already delivered to the wallet, through `sell_tokens`.
|
|
1635
|
-
*
|
|
1636
|
-
* The program needs the coin's pool: it keeps the vault out of the venue's account list. A mint
|
|
1637
|
-
* with no pool cannot be sold this way; use a plain venue trade for that.
|
|
1638
|
-
*/
|
|
1639
1511
|
async function sellTokens(client, input) {
|
|
1640
|
-
|
|
1641
|
-
if (
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1645
|
-
const venue = await resolveVenue(client, {
|
|
1646
|
-
mint,
|
|
1647
|
-
user: seller.address,
|
|
1648
|
-
...input.venue ? { venue: input.venue } : {}
|
|
1649
|
-
});
|
|
1650
|
-
const sell = getSellTokensInstruction({
|
|
1651
|
-
seller,
|
|
1652
|
-
pool: await poolAddress(mint),
|
|
1653
|
-
mint,
|
|
1654
|
-
userTokens: await associatedTokenAddress(seller.address, mint, pool.tokenProgram),
|
|
1655
|
-
venue: venue.program,
|
|
1656
|
-
feeCollectorWsol: await feeCollectorWsolAddress(),
|
|
1657
|
-
tokenProgram: pool.tokenProgram,
|
|
1658
|
-
amount,
|
|
1659
|
-
minQuoteOutput
|
|
1660
|
-
});
|
|
1661
|
-
return await buildMessage(client, seller, [...wsolPreparation(seller, venue), withRemainingAccounts(sell, venue.sellAccounts)], {
|
|
1662
|
-
addressLookupTables: input.addressLookupTables,
|
|
1663
|
-
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1664
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
/** Transfer a legacy resolve-only award into the purchaser's token account and close the draw. */
|
|
1668
|
-
async function claimPrize(client, input) {
|
|
1669
|
-
const { mint, purchaser, draw } = input;
|
|
1670
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1671
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1672
|
-
return await buildMessage(client, purchaser, [getClaimPrizeInstruction({
|
|
1673
|
-
purchaser,
|
|
1674
|
-
pool: await poolAddress(mint),
|
|
1675
|
-
draw,
|
|
1676
|
-
mint,
|
|
1677
|
-
vault: pool.vault,
|
|
1678
|
-
userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
|
|
1679
|
-
tokenProgram: pool.tokenProgram
|
|
1680
|
-
})], {
|
|
1681
|
-
addressLookupTables: input.addressLookupTables,
|
|
1682
|
-
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1683
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Claim and sell a legacy resolve-only draw in one transaction.
|
|
1688
|
-
*
|
|
1689
|
-
* The award is read off the draw so the caller can price the sale before signing. A draw that has
|
|
1690
|
-
* not resolved has no award yet, and this throws rather than building a sale of zero tokens.
|
|
1691
|
-
*/
|
|
1692
|
-
async function sellPrize(client, input) {
|
|
1693
|
-
const { mint, purchaser, draw, minQuoteOutput } = input;
|
|
1694
|
-
if (minQuoteOutput <= 0n) throw new Error("minQuoteOutput must be positive");
|
|
1695
|
-
const pool = await fetchPoolByMint(client, mint);
|
|
1696
|
-
if (!pool) throw new Error(`no gabox pool for mint ${mint}`);
|
|
1697
|
-
const record = await fetchDraw(client, draw);
|
|
1698
|
-
if (!record) throw new Error(`no draw at ${draw} — it may already be redeemed`);
|
|
1699
|
-
if (record.amount === 0n) throw new Error(`draw ${draw} has no award yet; it has not resolved`);
|
|
1512
|
+
if (input.amount <= 0n || input.minQuoteOutput <= 0n) throw new Error("amount and minQuoteOutput must be positive");
|
|
1513
|
+
if (input.maxNativeDebit < 0n) throw new Error("maxNativeDebit must not be negative");
|
|
1514
|
+
const pool = await fetchPoolByMint(client, input.mint);
|
|
1515
|
+
if (!pool) throw new Error(`no Gabox pool for mint ${input.mint}`);
|
|
1700
1516
|
const venue = await resolveVenue(client, {
|
|
1701
|
-
mint,
|
|
1702
|
-
user:
|
|
1517
|
+
mint: input.mint,
|
|
1518
|
+
user: input.seller.address,
|
|
1519
|
+
quote: {
|
|
1520
|
+
mint: pool.quoteMint,
|
|
1521
|
+
config: pool.quoteConfig,
|
|
1522
|
+
tokenProgram: pool.quoteTokenProgram
|
|
1523
|
+
},
|
|
1703
1524
|
...input.venue ? { venue: input.venue } : {}
|
|
1704
1525
|
});
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
mint,
|
|
1710
|
-
|
|
1711
|
-
userTokens: await associatedTokenAddress(purchaser.address, mint, pool.tokenProgram),
|
|
1712
|
-
tokenProgram: pool.tokenProgram,
|
|
1526
|
+
const gaboxPool = await poolAddress(input.mint);
|
|
1527
|
+
const sell = await getSellTokensInstructionAsync({
|
|
1528
|
+
seller: input.seller,
|
|
1529
|
+
pool: gaboxPool,
|
|
1530
|
+
mint: input.mint,
|
|
1531
|
+
quoteMint: pool.quoteMint,
|
|
1713
1532
|
venue: venue.program,
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
addressLookupTables: input.addressLookupTables,
|
|
1719
|
-
computeUnitLimit: input.computeUnitLimit ?? 3e5,
|
|
1720
|
-
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1533
|
+
quoteTokenProgram: pool.quoteTokenProgram,
|
|
1534
|
+
amount: input.amount,
|
|
1535
|
+
minQuoteOutput: input.minQuoteOutput,
|
|
1536
|
+
maxNativeDebit: input.maxNativeDebit
|
|
1721
1537
|
});
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
ata: wsolAccountFor(seller.address),
|
|
1733
|
-
owner: seller.address,
|
|
1734
|
-
mint: WSOL_MINT,
|
|
1735
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS$1
|
|
1736
|
-
})];
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* Quote the award held by a legacy open draw before the seller signs a floor.
|
|
1740
|
-
*
|
|
1741
|
-
* Read the draw, ask the venue, and subtract your own slippage tolerance to get `minQuoteOutput`.
|
|
1742
|
-
*/
|
|
1743
|
-
async function quoteSellPrize(client, mint, draw, user) {
|
|
1744
|
-
const record = await fetchDraw(client, draw);
|
|
1745
|
-
if (!record) throw new Error(`no draw at ${draw}`);
|
|
1746
|
-
const venue = await resolveVenue(client, {
|
|
1747
|
-
mint,
|
|
1748
|
-
user
|
|
1538
|
+
const leg = await quoteLegOut(client, {
|
|
1539
|
+
venue,
|
|
1540
|
+
seller: input.seller,
|
|
1541
|
+
minQuoteOutput: input.minQuoteOutput,
|
|
1542
|
+
receive: input.receive ?? "sol",
|
|
1543
|
+
gaboxAccounts: [
|
|
1544
|
+
gaboxPool,
|
|
1545
|
+
await vaultAddress(input.mint),
|
|
1546
|
+
await associatedTokenAddress(input.seller.address, input.mint)
|
|
1547
|
+
]
|
|
1749
1548
|
});
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1549
|
+
const instructions = [
|
|
1550
|
+
...leg.before,
|
|
1551
|
+
withRemainingAccounts(sell, venue.sellAccounts),
|
|
1552
|
+
...leg.after
|
|
1553
|
+
];
|
|
1554
|
+
try {
|
|
1555
|
+
return await buildMessage(client, input.seller, instructions, {
|
|
1556
|
+
addressLookupTables: {
|
|
1557
|
+
...input.addressLookupTables ?? client.addressLookupTables,
|
|
1558
|
+
...leg.lookupTables
|
|
1559
|
+
},
|
|
1560
|
+
computeUnitLimit: input.computeUnitLimit ?? computeUnitsWithRoute(125e3, leg),
|
|
1561
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1562
|
+
});
|
|
1563
|
+
} catch (cause) {
|
|
1564
|
+
throw routeSizeHint(cause, leg);
|
|
1565
|
+
}
|
|
1754
1566
|
}
|
|
1755
1567
|
//#endregion
|
|
1756
1568
|
//#region src/vrf.ts
|
|
@@ -1770,6 +1582,6 @@ async function oracleAccounts() {
|
|
|
1770
1582
|
};
|
|
1771
1583
|
}
|
|
1772
1584
|
//#endregion
|
|
1773
|
-
export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
|
|
1585
|
+
export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, BPS, BUY_PACK_COMPUTE_UNITS, CLAIM_COMPUTE_UNITS, CLUSTER_ENDPOINTS, COMPUTE_BUDGET_PROGRAM_ADDRESS, CPMM_ROUTE_COMPUTE_UNITS, CPMM_ROUTE_SLIPPAGE_BPS, CREATE_MACHINE_COMPUTE_UNITS, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_TIERS, DEVNET_ADDRESS_LOOKUP_TABLES, DEVNET_HTTP, DEVNET_LOOKUP_TABLE_ADDRESS, DEVNET_LOOKUP_TABLE_ADDRESSES, DEVNET_WS, DRAW_DISCRIMINATOR, DRAW_POOL_OFFSET, DRAW_PURCHASER_OFFSET, EXACT_IN_MARGIN_BPS, GABOX_PROGRAM_ID, GaboxMathError, IDENTITY_SEED, INSTRUCTIONS_SYSVAR, JUPITER_DEFAULT_COMPUTE_UNITS, JUPITER_DEFAULT_SLIPPAGE_BPS, JUPITER_LITE_URL, LAUNCH_DECIMALS, MAX_ATTEMPTS, MAX_COMPUTE_UNIT_LIMIT, MAX_MULTIPLIER_BPS, METAPLEX_PROGRAM_ADDRESS, MIN_SEED_MULTIPLIER_BPS, PACK_TOKENS, PLATFORM_ADMIN, POOL_CREATOR_OFFSET, POOL_DISCRIMINATOR, POOL_MINT_OFFSET, REDEEM_COMPUTE_UNITS, RETRY_SLOTS, SLOT_HASHES_SYSVAR, SYSTEM_PROGRAM_ADDRESS, TICKETS, TIERS, TIMEOUT_SLOTS, TOKEN_PROGRAM_ADDRESS, VRF_DEFAULT_QUEUE, VRF_PROGRAM_ADDRESS, WALLET_ACTIVITY_DISCRIMINATOR, WSOL_MINT, activityAddress, assertClusterUrl, assertRouteIsSafe, associatedTokenAddress, averageMultiplierBps, buildMessage, buyPack, choose, clusterNamedBy, computeBudgetInstructions, computeUnitsOf, computeUnitsWithRoute, createClient, createMachine, createQuoteAccount, decodeDraw, decodeEvent, decodeEvents, decodePool, decodeWalletActivity, defaultAddressLookupTables, defaultRoute, drawAddress, drawAvailability, expireDraw, fetchAddressLookupTables, fetchDraw, fetchEvents, fetchPoolAt, fetchPoolByMint, fetchPoolInventory, fetchVaultBalance, fetchWalletActivity, findActivityPda, findDrawPda, findIdentityPda, findPoolPda, findResolvedDraw, fundPrizes, fundWsol, generated_exports as generated, getOffer, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, jupiterRoute, listDraws, listDrawsByPool, listDrawsByPurchaser, listPools, maxMultiplierBps, offerFromState, oracleAccounts, poolAddress, providerOf, quote, quoteLegIn, quoteLegOut, quotePool, raydium_exports as raydium, raydiumCpmmRoute, resolveReservation, retryDraw, routeFrom, routeQuoteIn, routeQuoteOut, routeSizeHint, scopedVrfIdentityAddress, seedCostEstimate, seedShortfall, seedTokens, sellTokens, solPriceOf, tierAmount, tiersOf, uncappedMaximum, unwrapWsol, validatePack, validateTiers, vaultAddress, vrfIdentityAddress, websocketUrlFor, withRemainingAccounts };
|
|
1774
1586
|
|
|
1775
1587
|
//# sourceMappingURL=index.js.map
|