@gabox-labs/sdk 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +118 -34
- package/dist/gabox-DGTCh34U.js +2338 -0
- package/dist/gabox-DGTCh34U.js.map +1 -0
- package/dist/generated/index.d.ts +89 -36
- package/dist/generated/index.js +2 -2289
- package/dist/generated/index.js.map +1 -1
- package/dist/{index-BDfGvmgF.d.ts → index-CA0m7AKk.d.ts} +305 -54
- package/dist/index.d.ts +341 -34
- package/dist/index.js +880 -503
- package/dist/index.js.map +1 -1
- package/dist/raydium/index.d.ts +2 -2
- package/dist/raydium/index.js +2 -2
- package/dist/{raydium-B-l9V3O-.js → raydium-CU-tZzIk.js} +869 -191
- package/dist/raydium-CU-tZzIk.js.map +1 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/skills/gabox-sdk/SKILL.md +29 -14
- package/skills/gabox-sdk/references/api.md +82 -26
- package/dist/raydium-B-l9V3O-.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,457 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
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";
|
|
4
5
|
import { getCloseAccountInstruction, getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
|
|
5
6
|
import { getTransferSolInstruction } from "@solana-program/system";
|
|
6
|
-
//#region src/ids.ts
|
|
7
|
-
/** `declare_id!` in `programs/gabox/src/lib.rs`. */
|
|
8
|
-
const GABOX_PROGRAM_ID = GABOX_PROGRAM_ADDRESS;
|
|
9
|
-
/** MagicBlock's ephemeral VRF program. `vrf.rs` pins it and refuses any other. */
|
|
10
|
-
const VRF_PROGRAM_ADDRESS = "Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz";
|
|
11
|
-
/**
|
|
12
|
-
* MagicBlock's default oracle queue. The program pins this one address, so a pool creator cannot
|
|
13
|
-
* point a draw at an oracle they control.
|
|
14
|
-
*/
|
|
15
|
-
const VRF_DEFAULT_QUEUE = "Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh";
|
|
16
|
-
/** The seed of both identity PDAs. See `pdas.ts` — there are two, under different programs. */
|
|
17
|
-
const IDENTITY_SEED = new Uint8Array([
|
|
18
|
-
105,
|
|
19
|
-
100,
|
|
20
|
-
101,
|
|
21
|
-
110,
|
|
22
|
-
116,
|
|
23
|
-
105,
|
|
24
|
-
116,
|
|
25
|
-
121
|
|
26
|
-
]);
|
|
27
|
-
const SLOT_HASHES_SYSVAR = "SysvarS1otHashes111111111111111111111111111";
|
|
28
|
-
const INSTRUCTIONS_SYSVAR = "Sysvar1nstructions1111111111111111111111111";
|
|
29
|
-
/** `constants.rs`. A retry may not run sooner than this after the last attempt. */
|
|
30
|
-
const RETRY_SLOTS = 300n;
|
|
31
|
-
/** `constants.rs`. After this many slots from the purchase, anyone may expire the draw. */
|
|
32
|
-
const TIMEOUT_SLOTS = 216000n;
|
|
33
|
-
/** `constants.rs`. Three requests in total, counting the one `buy_pack` makes. */
|
|
34
|
-
const MAX_ATTEMPTS = 3;
|
|
35
|
-
/**
|
|
36
|
-
* `constants.rs`. Tokens in one pack, in base units: 1,000,000 tokens at the fixed 6 decimals, or
|
|
37
|
-
* 0.1% of the 1,000,000,000 supply. Every pool sells packs of this size. The venue decides what a
|
|
38
|
-
* pack costs, so the pack price follows the coin. Read `pool.packTokens` rather than this when a
|
|
39
|
-
* pool is at hand: a later program version may change the constant.
|
|
40
|
-
*/
|
|
41
|
-
const PACK_TOKENS = 1000000n * 1000000n;
|
|
42
|
-
//#endregion
|
|
43
|
-
//#region src/math.ts
|
|
44
|
-
/**
|
|
45
|
-
* A bigint port of `programs/gabox/src/math.rs`.
|
|
46
|
-
*
|
|
47
|
-
* The point is that a client can show a buyer the exact prize table the program will freeze into
|
|
48
|
-
* their `Draw`, before they pay. Every rounding step here matches the Rust, including the direction
|
|
49
|
-
* of each division. `test/math.test.ts` runs the same vectors as `programs/gabox/tests/math.rs`.
|
|
50
|
-
*
|
|
51
|
-
* All amounts are `bigint`, in the mint's smallest unit. `multiplierBps` and `tickets` are numbers
|
|
52
|
-
* because both are `u32` in the program and both stay small.
|
|
53
|
-
*/
|
|
54
|
-
/** Basis points. A multiplier of 10_000 pays back exactly one pack. */
|
|
55
|
-
const BPS = 10000n;
|
|
56
|
-
/**
|
|
57
|
-
* `constants::MIN_SEED_MULTIPLIER_BPS`. The mandatory seed makes a 3x top prize payable on the
|
|
58
|
-
* first pack, or the full top prize when the table's top tier is below 3x. A creator can add more
|
|
59
|
-
* seed on top through `extraSeedTokens`.
|
|
60
|
-
*/
|
|
61
|
-
const MIN_SEED_MULTIPLIER_BPS = 3e4;
|
|
62
|
-
/**
|
|
63
|
-
* `constants::MAX_MULTIPLIER_BPS`. A table's top tier can be at most 20x. Seed beyond what a 20x
|
|
64
|
-
* prize needs stays in the vault as backup for the draws after a top-tier hit.
|
|
65
|
-
*/
|
|
66
|
-
const MAX_MULTIPLIER_BPS = 2e5;
|
|
67
|
-
/** Ticket counts must sum to this. A uniform 16-bit word then maps with no modulo bias. */
|
|
68
|
-
const TICKETS = 65536;
|
|
69
|
-
/** The prize table has exactly this many slots. Unused slots are all-zero. */
|
|
70
|
-
const TIERS = 8;
|
|
71
|
-
/** Thrown by every function here. `code` matches a `GaboxError` variant name. */
|
|
72
|
-
var GaboxMathError = class extends Error {
|
|
73
|
-
code;
|
|
74
|
-
constructor(code, message) {
|
|
75
|
-
super(`${code}: ${message}`);
|
|
76
|
-
this.name = "GaboxMathError";
|
|
77
|
-
this.code = code;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
const U64_MAX = (1n << 64n) - 1n;
|
|
81
|
-
const U32_MAX = 4294967295;
|
|
82
|
-
/**
|
|
83
|
-
* The default prize table. The program does not enforce this table: it accepts any table that
|
|
84
|
-
* passes `validateTiers`. This is only the table the Gabox app creates its pools with, and the
|
|
85
|
-
* starting point for a client that has no reason to pick another one.
|
|
86
|
-
*
|
|
87
|
-
* Common 75% at 0.52x, Rare 20% at 1.2x, Epic 4% at 3x, Mythic 1% at 20x. Expected payout is
|
|
88
|
-
* 0.9499x of a pack, so the seed is 19 packs.
|
|
89
|
-
*/
|
|
90
|
-
const DEFAULT_TIERS = Object.freeze([
|
|
91
|
-
Object.freeze({
|
|
92
|
-
multiplierBps: 5201,
|
|
93
|
-
tickets: 49153
|
|
94
|
-
}),
|
|
95
|
-
Object.freeze({
|
|
96
|
-
multiplierBps: 12e3,
|
|
97
|
-
tickets: 13107
|
|
98
|
-
}),
|
|
99
|
-
Object.freeze({
|
|
100
|
-
multiplierBps: 3e4,
|
|
101
|
-
tickets: 2621
|
|
102
|
-
}),
|
|
103
|
-
Object.freeze({
|
|
104
|
-
multiplierBps: 2e5,
|
|
105
|
-
tickets: 655
|
|
106
|
-
}),
|
|
107
|
-
Object.freeze({
|
|
108
|
-
multiplierBps: 0,
|
|
109
|
-
tickets: 0
|
|
110
|
-
}),
|
|
111
|
-
Object.freeze({
|
|
112
|
-
multiplierBps: 0,
|
|
113
|
-
tickets: 0
|
|
114
|
-
}),
|
|
115
|
-
Object.freeze({
|
|
116
|
-
multiplierBps: 0,
|
|
117
|
-
tickets: 0
|
|
118
|
-
}),
|
|
119
|
-
Object.freeze({
|
|
120
|
-
multiplierBps: 0,
|
|
121
|
-
tickets: 0
|
|
122
|
-
})
|
|
123
|
-
]);
|
|
124
|
-
function checkedU64(value, what) {
|
|
125
|
-
if (value < 0n || value > U64_MAX) throw new GaboxMathError("Arithmetic", `${what} does not fit in u64`);
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
/** Tier fields encode as program `u32`s. Reject JavaScript-only values before `BigInt` or codecs. */
|
|
129
|
-
function checkedU32(value, what) {
|
|
130
|
-
if (!Number.isInteger(value) || value < 0 || value > U32_MAX) throw new GaboxMathError("InvalidDistribution", `${what} must be an integer from 0 to ${U32_MAX}`);
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
/** `math::tokens`. Floor division, and an overflow past u64 is an error, not a wrap. */
|
|
134
|
-
function tierAmount(base, multiplierBps) {
|
|
135
|
-
return checkedU64(base * BigInt(checkedU32(multiplierBps, "multiplierBps")) / BPS, "tier amount");
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* `math::validate`. Checks the table alone, with no base.
|
|
139
|
-
*
|
|
140
|
-
* Four rules, and each one closes a way to sell a bad ticket:
|
|
141
|
-
* - ticket counts sum to exactly 65,536, so the 16-bit draw is uniform;
|
|
142
|
-
* - an unused row is all-zero, so a hidden multiplier cannot ride along;
|
|
143
|
-
* - a ticketed tier's multiplier is from 1 to `MAX_MULTIPLIER_BPS` (20x);
|
|
144
|
-
* - the expected multiplier over all tickets is at most 1x, so the table cannot promise more
|
|
145
|
-
* tokens than a pack buys. This bounds tokens, not cash value.
|
|
146
|
-
*/
|
|
147
|
-
function validateTiers(tiers) {
|
|
148
|
-
if (tiers.length !== 8) throw new GaboxMathError("InvalidDistribution", `expected 8 tiers, got ${tiers.length}`);
|
|
149
|
-
let count = 0n;
|
|
150
|
-
let expected = 0n;
|
|
151
|
-
for (const [index, tier] of tiers.entries()) {
|
|
152
|
-
checkedU32(tier.tickets, `tier ${index} tickets`);
|
|
153
|
-
checkedU32(tier.multiplierBps, `tier ${index} multiplierBps`);
|
|
154
|
-
if (tier.tickets === 0) {
|
|
155
|
-
if (tier.multiplierBps !== 0) throw new GaboxMathError("InvalidDistribution", "a tier with no tickets must have no multiplier");
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (tier.multiplierBps <= 0) throw new GaboxMathError("InvalidDistribution", "a ticketed tier must pay something; it would sell a ticket that can only win zero");
|
|
159
|
-
if (tier.multiplierBps > 2e5) throw new GaboxMathError("InvalidDistribution", `tier ${index} multiplierBps exceeds the ${MAX_MULTIPLIER_BPS} maximum (20x)`);
|
|
160
|
-
count += BigInt(tier.tickets);
|
|
161
|
-
expected += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
162
|
-
}
|
|
163
|
-
if (count !== BigInt(65536)) throw new GaboxMathError("InvalidDistribution", `tickets must sum to ${TICKETS}, they sum to ${count}`);
|
|
164
|
-
if (expected > 10000n * BigInt(65536)) throw new GaboxMathError("UnfundedExpectation", "the expected token award exceeds the tokens a pack buys");
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* `math::validate_pack`. Every ticketed tier must pay at least one token for a pack of this size.
|
|
168
|
-
*
|
|
169
|
-
* The program checks this once at creation. At `PACK_TOKENS` no sane table fails it; it exists so
|
|
170
|
-
* a table cannot sell a ticket that can only win zero.
|
|
171
|
-
*/
|
|
172
|
-
function validatePack(packTokens, tiers) {
|
|
173
|
-
if (packTokens <= 0n) throw new GaboxMathError("ZeroAmount", "packTokens must be positive");
|
|
174
|
-
checkedU64(packTokens, "pack tokens");
|
|
175
|
-
validateTiers(tiers);
|
|
176
|
-
for (const [i, tier] of tiers.entries()) {
|
|
177
|
-
if (tier.tickets === 0) continue;
|
|
178
|
-
if (tierAmount(packTokens, tier.multiplierBps) === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* `math::seed_tokens`. The mandatory seed a table needs, in tokens.
|
|
183
|
-
*
|
|
184
|
-
* The mandatory seed makes a 3x top prize payable on the first pack, or the full top prize when
|
|
185
|
-
* the table's top tier is below 3x. A jackpot above 3x still shows the full table once later
|
|
186
|
-
* packs grow the free vault; the mandatory seed alone does not have to fund it. The pack itself
|
|
187
|
-
* brings `packTokens` into the vault, so the vault has to hold the rest beforehand:
|
|
188
|
-
*
|
|
189
|
-
* seedTokens = min(uncappedMaximum(packTokens, tiers), tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS)) - packTokens
|
|
190
|
-
*
|
|
191
|
-
* A 20x or a 3x top tier on a 1M-token pack both need a 2M-token seed, since the mandatory seed
|
|
192
|
-
* stops at the 3x minimum. A 2x top tier needs a 1M-token seed, already below the minimum. A
|
|
193
|
-
* table whose top tier pays exactly one pack needs no seed. A top tier below one pack is refused:
|
|
194
|
-
* every ticket would lose. `createMachine` lets a creator add more seed on top of this mandatory
|
|
195
|
-
* amount through `extraSeedTokens`, for example to fund a top tier above 3x from the first pack.
|
|
196
|
-
*
|
|
197
|
-
* Pool initialization computes this number itself and buys exactly that many tokens on the curve.
|
|
198
|
-
* The creator only signs a maximum SOL cost.
|
|
199
|
-
*/
|
|
200
|
-
function seedTokens(packTokens, tiers) {
|
|
201
|
-
validateTiers(tiers);
|
|
202
|
-
const largest = uncappedMaximum(packTokens, tiers);
|
|
203
|
-
if (largest < packTokens) throw new GaboxMathError("JackpotBelowOnePack", "the largest tier must pay at least one pack");
|
|
204
|
-
const minimum = tierAmount(packTokens, MIN_SEED_MULTIPLIER_BPS);
|
|
205
|
-
return (largest < minimum ? largest : minimum) - packTokens;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* `math::uncapped_maximum`. The largest tier's award for this base, before any inventory cap.
|
|
209
|
-
*
|
|
210
|
-
* At `packTokens` this is the jackpot in tokens. `seedTokens` is this minus one pack.
|
|
211
|
-
*/
|
|
212
|
-
function uncappedMaximum(base, tiers) {
|
|
213
|
-
validateTiers(tiers);
|
|
214
|
-
let largest = 0n;
|
|
215
|
-
for (const tier of tiers) {
|
|
216
|
-
if (tier.tickets === 0) continue;
|
|
217
|
-
const amount = tierAmount(base, tier.multiplierBps);
|
|
218
|
-
if (amount > largest) largest = amount;
|
|
219
|
-
}
|
|
220
|
-
return largest;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* `math::quote`. The offer a pack of `base` tokens would freeze right now. `base` is always
|
|
224
|
-
* `pool.packTokens`; the parameter stays general so the vectors can use small numbers.
|
|
225
|
-
*
|
|
226
|
-
* `inventory` is the vault's token balance and `reserved` is `pool.reserved`. The difference is
|
|
227
|
-
* free inventory. This pack's own `base` is added to it, because the purchase and the offer are
|
|
228
|
-
* one transaction — the tokens are in the vault before the draw is written.
|
|
229
|
-
*
|
|
230
|
-
* Every amount is capped at what is actually available. A tier that rounds to zero tokens is an
|
|
231
|
-
* error: the pool must not sell a ticket that can only win nothing.
|
|
232
|
-
*/
|
|
233
|
-
function quote(base, tiers, inventory, reserved) {
|
|
234
|
-
validateTiers(tiers);
|
|
235
|
-
if (inventory < reserved) throw new GaboxMathError("InsolventInventory", "the vault holds less than the pool has already reserved");
|
|
236
|
-
const available = checkedU64(inventory - reserved + base, "available inventory");
|
|
237
|
-
const prizes = Array.from({ length: 8 }, () => ({
|
|
238
|
-
amount: 0n,
|
|
239
|
-
tickets: 0
|
|
240
|
-
}));
|
|
241
|
-
let maximum = 0n;
|
|
242
|
-
let minimum = U64_MAX;
|
|
243
|
-
for (const [i, tier] of tiers.entries()) {
|
|
244
|
-
if (tier.tickets === 0) continue;
|
|
245
|
-
const uncapped = tierAmount(base, tier.multiplierBps);
|
|
246
|
-
if (uncapped === 0n) throw new GaboxMathError("PackTooSmall", `tier ${i} rounds to zero tokens at this pack size`);
|
|
247
|
-
const amount = uncapped < available ? uncapped : available;
|
|
248
|
-
prizes[i] = {
|
|
249
|
-
amount,
|
|
250
|
-
tickets: tier.tickets
|
|
251
|
-
};
|
|
252
|
-
if (amount > maximum) maximum = amount;
|
|
253
|
-
if (amount < minimum) minimum = amount;
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
prizes,
|
|
257
|
-
maximum,
|
|
258
|
-
minimum
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* `math::choose`. Which prize a 16-bit ticket wins.
|
|
263
|
-
*
|
|
264
|
-
* The rows are consecutive ranges in table order, so this is a running total and a comparison.
|
|
265
|
-
* A ticket past the last row returns `0`, which cannot happen for a validated table.
|
|
266
|
-
*/
|
|
267
|
-
function choose(prizes, ticket) {
|
|
268
|
-
let end = 0;
|
|
269
|
-
for (const prize of prizes) {
|
|
270
|
-
end += prize.tickets;
|
|
271
|
-
if (ticket < end) return prize.amount;
|
|
272
|
-
}
|
|
273
|
-
return 0n;
|
|
274
|
-
}
|
|
275
|
-
/** `math::resolve_reservation`. Release the unwon part of a maximum, keep the award reserved. */
|
|
276
|
-
function resolveReservation(reserved, maximum, award) {
|
|
277
|
-
if (award > maximum) throw new GaboxMathError("InsolventInventory", "the award exceeds the reserved maximum");
|
|
278
|
-
if (reserved < maximum) throw new GaboxMathError("Arithmetic", "the pool has reserved less than this draw holds");
|
|
279
|
-
return checkedU64(reserved - maximum + award, "reserved");
|
|
280
|
-
}
|
|
281
|
-
/** The largest `multiplierBps` on any ticketed row. `0` for a table with no rows. */
|
|
282
|
-
function maxMultiplierBps(tiers) {
|
|
283
|
-
validateTiers(tiers);
|
|
284
|
-
let largest = 0;
|
|
285
|
-
for (const tier of tiers) {
|
|
286
|
-
if (tier.tickets === 0) continue;
|
|
287
|
-
if (tier.multiplierBps > largest) largest = tier.multiplierBps;
|
|
288
|
-
}
|
|
289
|
-
return largest;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* The expected multiplier over all 65,536 tickets, in basis points. Rounded down.
|
|
293
|
-
*
|
|
294
|
-
* `validateTiers` caps this at 10,000. A table at 9,800 keeps 2% of every pack's tokens in the
|
|
295
|
-
* vault on average, which is what lets a pool survive a run of top-tier wins.
|
|
296
|
-
*/
|
|
297
|
-
function averageMultiplierBps(tiers) {
|
|
298
|
-
validateTiers(tiers);
|
|
299
|
-
let weighted = 0n;
|
|
300
|
-
for (const tier of tiers) {
|
|
301
|
-
if (tier.tickets === 0) continue;
|
|
302
|
-
weighted += BigInt(tier.multiplierBps) * BigInt(tier.tickets);
|
|
303
|
-
}
|
|
304
|
-
return Number(weighted / BigInt(TICKETS));
|
|
305
|
-
}
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region src/pdas.ts
|
|
308
|
-
/**
|
|
309
|
-
* Gabox's own program-derived addresses.
|
|
310
|
-
*
|
|
311
|
-
* Raydium's live in `src/raydium/pdas.ts`, because they depend on the cluster. These do not: the
|
|
312
|
-
* Gabox program id is the same everywhere.
|
|
313
|
-
*
|
|
314
|
-
* Classic SPL Token only. Both venues use it, and the accepted LaunchLab create instruction mints
|
|
315
|
-
* the coin with it, so no derivation here takes a token program any more.
|
|
316
|
-
*/
|
|
317
|
-
/** `["pool", mint]`. One pool per coin. */
|
|
318
|
-
const poolAddress = async (mint) => (await findPoolPda({ mint }))[0];
|
|
319
|
-
/** `["draw", pool, seq]`, with the sequence as eight little-endian bytes. */
|
|
320
|
-
const drawAddress = async (pool, seq) => (await findDrawPda({
|
|
321
|
-
pool,
|
|
322
|
-
seq
|
|
323
|
-
}))[0];
|
|
324
|
-
/** The per-wallet `WalletActivity` PDA. `buy_pack` derives it from the purchaser automatically. */
|
|
325
|
-
const activityAddress = async (wallet) => (await findActivityPda({ purchaser: wallet }))[0];
|
|
326
|
-
/** `["identity"]` under Gabox. The PDA the program signs its randomness request with. */
|
|
327
|
-
const vrfIdentityAddress = async () => (await findIdentityPda())[0];
|
|
328
|
-
/** `["identity", gabox_program_id]` under the VRF program. MagicBlock signs the callback with it. */
|
|
329
|
-
async function scopedVrfIdentityAddress() {
|
|
330
|
-
return await getProgramDerivedAddress({
|
|
331
|
-
programAddress: VRF_PROGRAM_ADDRESS,
|
|
332
|
-
seeds: [getBytesEncoder().encode(IDENTITY_SEED), getAddressEncoder().encode(GABOX_PROGRAM_ID)]
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
/** An associated token account under classic SPL Token. */
|
|
336
|
-
const associatedTokenAddress = async (owner, mint, tokenProgram = TOKEN_PROGRAM_ADDRESS) => await ata(owner, mint, tokenProgram);
|
|
337
|
-
/** The prize vault: the pool PDA's own associated token account for the coin. */
|
|
338
|
-
const vaultAddress = async (mint) => await associatedTokenAddress(await poolAddress(mint), mint);
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region src/accounts.ts
|
|
341
|
-
/** Account readers and bounded discovery scans. Old pool layouts are intentionally unsupported. */
|
|
342
|
-
const DISCRIMINATOR = 8;
|
|
343
|
-
const DRAW_POOL_OFFSET = DISCRIMINATOR;
|
|
344
|
-
const DRAW_PURCHASER_OFFSET = 40;
|
|
345
|
-
const POOL_CREATOR_OFFSET = DISCRIMINATOR;
|
|
346
|
-
const POOL_MINT_OFFSET = 40;
|
|
347
|
-
const base58 = getBase58Decoder();
|
|
348
|
-
const base64 = getBase64Encoder();
|
|
349
|
-
const asBase58 = (bytes) => base58.decode(bytes);
|
|
350
|
-
const memcmp = (offset, bytes) => ({ memcmp: {
|
|
351
|
-
offset: BigInt(offset),
|
|
352
|
-
bytes,
|
|
353
|
-
encoding: "base58"
|
|
354
|
-
} });
|
|
355
|
-
const encoded = (address, data) => ({
|
|
356
|
-
address,
|
|
357
|
-
data,
|
|
358
|
-
exists: true,
|
|
359
|
-
executable: false,
|
|
360
|
-
lamports: 0n,
|
|
361
|
-
programAddress: GABOX_PROGRAM_ID,
|
|
362
|
-
space: BigInt(data.length)
|
|
363
|
-
});
|
|
364
|
-
async function fetchPoolByMint(client, mint) {
|
|
365
|
-
return await fetchPoolAt(client, await poolAddress(mint));
|
|
366
|
-
}
|
|
367
|
-
async function fetchPoolAt(client, address) {
|
|
368
|
-
return decodeCurrentPool(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
369
|
-
}
|
|
370
|
-
function decodeCurrentPool(account) {
|
|
371
|
-
if (!account.exists) return null;
|
|
372
|
-
if (account.data.length !== getPoolSize()) throw new Error(`pool ${account.address} is not a Gabox pool and is intentionally unsupported`);
|
|
373
|
-
return decodePool(account).data;
|
|
374
|
-
}
|
|
375
|
-
function decodeCurrentDraw(account) {
|
|
376
|
-
if (!account.exists) return null;
|
|
377
|
-
if (account.data.length !== getDrawSize()) throw new Error(`draw ${account.address} is not a Gabox draw and is intentionally unsupported`);
|
|
378
|
-
return decodeDraw(account).data;
|
|
379
|
-
}
|
|
380
|
-
async function fetchDraw(client, address) {
|
|
381
|
-
return decodeCurrentDraw(await fetchEncodedAccount(client.rpc, address, { commitment: "confirmed" }));
|
|
382
|
-
}
|
|
383
|
-
function decodeCurrentWalletActivity(account) {
|
|
384
|
-
if (!account.exists) return null;
|
|
385
|
-
if (account.data.length !== getWalletActivitySize()) throw new Error(`activity account ${account.address} is not a Gabox WalletActivity and is intentionally unsupported`);
|
|
386
|
-
return decodeWalletActivity(account).data;
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* A wallet's lifetime pack-buying activity, or `null` before it has bought its first pack.
|
|
390
|
-
* `buy_pack` creates this account `init_if_needed`, so a fresh wallet has none yet.
|
|
391
|
-
*/
|
|
392
|
-
async function fetchWalletActivity(client, wallet) {
|
|
393
|
-
return decodeCurrentWalletActivity(await fetchEncodedAccount(client.rpc, await activityAddress(wallet), { commitment: "confirmed" }));
|
|
394
|
-
}
|
|
395
|
-
async function scan(rpc, filters, decode) {
|
|
396
|
-
return (await rpc.getProgramAccounts(GABOX_PROGRAM_ID, {
|
|
397
|
-
encoding: "base64",
|
|
398
|
-
commitment: "confirmed",
|
|
399
|
-
filters
|
|
400
|
-
}).send()).map(({ pubkey, account }) => decode(pubkey, new Uint8Array(base64.encode(account.data[0]))));
|
|
401
|
-
}
|
|
402
|
-
async function listPools(client) {
|
|
403
|
-
return await scan(client.rpc, [memcmp(0, asBase58(POOL_DISCRIMINATOR)), { dataSize: BigInt(getPoolSize()) }], (address, data) => ({
|
|
404
|
-
address,
|
|
405
|
-
data: decodePool(encoded(address, data)).data
|
|
406
|
-
}));
|
|
407
|
-
}
|
|
408
|
-
async function listDraws(client, query = {}) {
|
|
409
|
-
const filters = [memcmp(0, asBase58(DRAW_DISCRIMINATOR)), { dataSize: BigInt(getDrawSize()) }];
|
|
410
|
-
if (query.pool) filters.push(memcmp(8, query.pool));
|
|
411
|
-
if (query.purchaser) filters.push(memcmp(40, query.purchaser));
|
|
412
|
-
return await scan(client.rpc, filters, (address, data) => ({
|
|
413
|
-
address,
|
|
414
|
-
data: decodeDraw(encoded(address, data)).data
|
|
415
|
-
}));
|
|
416
|
-
}
|
|
417
|
-
const listDrawsByPool = async (client, pool) => await listDraws(client, { pool });
|
|
418
|
-
const listDrawsByPurchaser = async (client, purchaser) => await listDraws(client, { purchaser });
|
|
419
|
-
async function fetchVaultBalance(client, mint) {
|
|
420
|
-
const vault = await vaultAddress(mint);
|
|
421
|
-
const { value } = await client.rpc.getAccountInfo(vault, {
|
|
422
|
-
encoding: "base64",
|
|
423
|
-
commitment: "confirmed"
|
|
424
|
-
}).send();
|
|
425
|
-
return value ? tokenAccountAmount(new Uint8Array(base64.encode(value.data[0]))) : 0n;
|
|
426
|
-
}
|
|
427
|
-
async function fetchPoolInventory(client, mint) {
|
|
428
|
-
const poolPda = await poolAddress(mint);
|
|
429
|
-
const vaultPda = await vaultAddress(mint);
|
|
430
|
-
const { value } = await client.rpc.getMultipleAccounts([poolPda, vaultPda], {
|
|
431
|
-
encoding: "base64",
|
|
432
|
-
commitment: "confirmed"
|
|
433
|
-
}).send();
|
|
434
|
-
const poolAccount = value[0];
|
|
435
|
-
if (!poolAccount) return null;
|
|
436
|
-
const pool = decodeCurrentPool(encoded(poolPda, new Uint8Array(base64.encode(poolAccount.data[0]))));
|
|
437
|
-
if (!pool) return null;
|
|
438
|
-
if (pool.vault !== vaultPda) throw new Error(`pool ${poolPda} has an unexpected vault; it is not a supported Gabox pool`);
|
|
439
|
-
const vaultAccount = value[1];
|
|
440
|
-
const inventory = vaultAccount ? tokenAccountAmount(new Uint8Array(base64.encode(vaultAccount.data[0]))) : 0n;
|
|
441
|
-
return {
|
|
442
|
-
poolAddress: poolPda,
|
|
443
|
-
pool,
|
|
444
|
-
inventory,
|
|
445
|
-
reserved: pool.reserved,
|
|
446
|
-
free: inventory > pool.reserved ? inventory - pool.reserved : 0n
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
const tiersOf = (pool) => pool.tiers.map(({ multiplierBps, tickets }) => ({
|
|
450
|
-
multiplierBps,
|
|
451
|
-
tickets
|
|
452
|
-
}));
|
|
453
|
-
const quotePool = (inventory) => quote(inventory.pool.packTokens, tiersOf(inventory.pool), inventory.inventory, inventory.reserved);
|
|
454
|
-
//#endregion
|
|
455
7
|
//#region src/events.ts
|
|
456
8
|
/** Decode Gabox's Anchor events. Draw accounts are closed on delivery; use `DrawResolved` as final state. */
|
|
457
9
|
const b64 = getBase64Encoder();
|
|
@@ -583,7 +135,7 @@ async function findResolvedDraw(client, address) {
|
|
|
583
135
|
//#endregion
|
|
584
136
|
//#region src/lookupTables.ts
|
|
585
137
|
/**
|
|
586
|
-
* Shared devnet address lookup table, verified on 2026-09-
|
|
138
|
+
* Shared devnet address lookup table, verified on 2026-09-18T11:04:06.369Z.
|
|
587
139
|
* Generated by scripts/deploy-lookup-table.ts. Existing indices are immutable;
|
|
588
140
|
* keep this table active while clients use it. Authority is the devnet deploy wallet.
|
|
589
141
|
*/
|
|
@@ -702,7 +254,14 @@ const DEVNET_LOOKUP_TABLE_ADDRESSES = [
|
|
|
702
254
|
address("DRaycpLY18LhpbydsBWbVJtxpNv9oXPgjRSfpF2bWpYb"),
|
|
703
255
|
address("CXniRufdq5xL8t8jZAPxsPZDpuudwuJSPWnbcD5Y5Nxq"),
|
|
704
256
|
address("G7YfJJp1TX1VtzN4V2yhPNSU23AKPSy1U2miRdwAByK5"),
|
|
705
|
-
address("5WcPTEQ59UqpQzjZUPbU8QRGCbj7NeQNLDa7DbsLkLKT")
|
|
257
|
+
address("5WcPTEQ59UqpQzjZUPbU8QRGCbj7NeQNLDa7DbsLkLKT"),
|
|
258
|
+
address("USDCoctVLVnvTXBEuP9s8hntucdJokbo17RwHuNXemT"),
|
|
259
|
+
address("4wHbNkobu7iARU9MbCEqDSAq6JuQreGupG2Jsf2R3DFP"),
|
|
260
|
+
address("5Eu2G2USTy1pqphmQzQ2SBXWrBq5sdhgEh7hso9R2xix"),
|
|
261
|
+
address("A9qBhPy4k5UYW72hSgAkh1Epr2do69P54yzzcMV3yv6b"),
|
|
262
|
+
address("Aw93pmXP52u6WSW2HcafRxua1LDht5MZhhXaaR7qCjsN"),
|
|
263
|
+
address("CPLUA2NTYSGjsB1E9iXT3MrPn69WRFJvKTdJZw5NdEjh"),
|
|
264
|
+
address("7LnqjXdqJEdccWZQs5YJobQ8MDmcK4sG2oo4Ty4LBC8c")
|
|
706
265
|
];
|
|
707
266
|
const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET_LOOKUP_TABLE_ADDRESSES] };
|
|
708
267
|
/**
|
|
@@ -716,29 +275,207 @@ const DEVNET_ADDRESS_LOOKUP_TABLES = { [DEVNET_LOOKUP_TABLE_ADDRESS]: [...DEVNET
|
|
|
716
275
|
function defaultAddressLookupTables(cluster) {
|
|
717
276
|
return cluster === "devnet" ? { ...DEVNET_ADDRESS_LOOKUP_TABLES } : {};
|
|
718
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);
|
|
719
435
|
//#endregion
|
|
720
436
|
//#region src/offer.ts
|
|
721
437
|
/**
|
|
722
|
-
* 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`.
|
|
723
441
|
*
|
|
724
442
|
* Throws when the coin has no pool.
|
|
725
443
|
*/
|
|
726
444
|
async function getOffer(client, mint, options = {}) {
|
|
727
445
|
const inventory = await fetchPoolInventory(client, mint);
|
|
728
446
|
if (!inventory) throw new Error(`no gabox pool for mint ${mint}`);
|
|
447
|
+
const { pool } = inventory;
|
|
729
448
|
const venue = await resolveVenue(client, {
|
|
730
449
|
mint,
|
|
731
|
-
user: options.user ??
|
|
450
|
+
user: options.user ?? pool.creator,
|
|
451
|
+
quote: {
|
|
452
|
+
mint: pool.quoteMint,
|
|
453
|
+
config: pool.quoteConfig,
|
|
454
|
+
tokenProgram: pool.quoteTokenProgram
|
|
455
|
+
},
|
|
732
456
|
...options.venue ? { venue: options.venue } : {}
|
|
733
457
|
});
|
|
734
|
-
|
|
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
|
+
});
|
|
735
465
|
}
|
|
736
466
|
/**
|
|
737
467
|
* The same computation with the reads already done. Useful when a caller holds a `ResolvedVenue`
|
|
738
468
|
* and wants to re-price without touching the network. `quoteAmount` is
|
|
739
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.
|
|
740
473
|
*/
|
|
741
|
-
function offerFromState(inventory, venue, quoteAmount
|
|
474
|
+
function offerFromState(inventory, venue, quoteAmount, display = {
|
|
475
|
+
quoteDecimals: 0,
|
|
476
|
+
quoteSymbol: null,
|
|
477
|
+
solAmount: null
|
|
478
|
+
}) {
|
|
742
479
|
const { pool } = inventory;
|
|
743
480
|
const tiers = tiersOf(pool);
|
|
744
481
|
const offer = quote(pool.packTokens, tiers, inventory.inventory, inventory.reserved);
|
|
@@ -749,6 +486,9 @@ function offerFromState(inventory, venue, quoteAmount) {
|
|
|
749
486
|
packTokens: pool.packTokens,
|
|
750
487
|
quoteAmount,
|
|
751
488
|
quoteMint: pool.quoteMint,
|
|
489
|
+
quoteDecimals: display.quoteDecimals,
|
|
490
|
+
quoteSymbol: display.quoteSymbol,
|
|
491
|
+
solAmount: display.solAmount,
|
|
752
492
|
seedQuoteAmount: pool.seedQuoteAmount,
|
|
753
493
|
seedTokens: seedTokens(pool.packTokens, tiers),
|
|
754
494
|
venue,
|
|
@@ -777,6 +517,374 @@ function seedShortfall(offer) {
|
|
|
777
517
|
return offer.free >= needed ? 0n : needed - offer.free;
|
|
778
518
|
}
|
|
779
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.
|
|
775
|
+
*
|
|
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.
|
|
780
|
+
*/
|
|
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)
|
|
823
|
+
});
|
|
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;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
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.
|
|
848
|
+
*/
|
|
849
|
+
async function routeFrom(client, response, amounts) {
|
|
850
|
+
return {
|
|
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
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* The unit limit Jupiter asked for, or `JUPITER_DEFAULT_COMPUTE_UNITS` when it asked for none.
|
|
863
|
+
*
|
|
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.
|
|
866
|
+
*/
|
|
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
|
+
};
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
780
888
|
//#region src/rpc.ts
|
|
781
889
|
/**
|
|
782
890
|
* The client, and the cluster guard.
|
|
@@ -866,9 +974,19 @@ function createClient(config) {
|
|
|
866
974
|
wsUrl,
|
|
867
975
|
rpc: createSolanaRpc(url),
|
|
868
976
|
rpcSubscriptions: createSolanaRpcSubscriptions(wsUrl),
|
|
869
|
-
addressLookupTables: config.addressLookupTables ?? defaultAddressLookupTables(cluster)
|
|
977
|
+
addressLookupTables: config.addressLookupTables ?? defaultAddressLookupTables(cluster),
|
|
978
|
+
route: config.route === void 0 ? defaultRoute(cluster) : config.route
|
|
870
979
|
};
|
|
871
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* The swap provider a cluster gets when the caller names none.
|
|
983
|
+
*
|
|
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.
|
|
986
|
+
*/
|
|
987
|
+
function defaultRoute(cluster) {
|
|
988
|
+
return cluster === "mainnet-beta" ? jupiterRoute() : null;
|
|
989
|
+
}
|
|
872
990
|
//#endregion
|
|
873
991
|
//#region src/tx/wsol.ts
|
|
874
992
|
/**
|
|
@@ -922,6 +1040,176 @@ function unwrapWsol(owner, account) {
|
|
|
922
1040
|
});
|
|
923
1041
|
}
|
|
924
1042
|
//#endregion
|
|
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
|
+
* The third shape belongs to packs only. A machine's seed is always paid by its creator, in the
|
|
1064
|
+
* machine's own quote token, so `createMachine` uses `quoteLegFromWallet` and never swaps.
|
|
1065
|
+
*
|
|
1066
|
+
* Nothing here ever splits the work across two transactions. A swap that settles separately would
|
|
1067
|
+
* leave the wallet holding a token it never asked for whenever the second half failed.
|
|
1068
|
+
*/
|
|
1069
|
+
/** Create the wallet's quote account if it is missing. Idempotent, so a second create is free. */
|
|
1070
|
+
function createQuoteAccount(owner, venue) {
|
|
1071
|
+
return getCreateAssociatedTokenIdempotentInstruction({
|
|
1072
|
+
payer: owner,
|
|
1073
|
+
ata: venue.userQuoteToken,
|
|
1074
|
+
owner: owner.address,
|
|
1075
|
+
mint: venue.quoteMint,
|
|
1076
|
+
tokenProgram: venue.quoteTokenProgram
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* The leg for a wallet that already holds what it is about to spend. No swap.
|
|
1081
|
+
*
|
|
1082
|
+
* A WSOL pool is the one case where "already holds it" means lamports, so those are wrapped and the
|
|
1083
|
+
* account is closed again afterwards. Any other quote only needs its account to exist; closing it
|
|
1084
|
+
* would throw away a real balance.
|
|
1085
|
+
*
|
|
1086
|
+
* `createMachine` uses this and nothing else: the seed is always the creator's own money, in the
|
|
1087
|
+
* machine's quote token.
|
|
1088
|
+
*/
|
|
1089
|
+
async function quoteLegFromWallet(venue, payer, maxQuoteIn) {
|
|
1090
|
+
if (venue.quoteMint === "So11111111111111111111111111111111111111112") {
|
|
1091
|
+
const wsol = await fundWsol(payer, maxQuoteIn);
|
|
1092
|
+
return {
|
|
1093
|
+
before: wsol.instructions,
|
|
1094
|
+
after: [unwrapWsol(payer, wsol.account)],
|
|
1095
|
+
lookupTables: {},
|
|
1096
|
+
mode: null,
|
|
1097
|
+
solAmount: maxQuoteIn,
|
|
1098
|
+
computeUnits: 0
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
before: [createQuoteAccount(payer, venue)],
|
|
1103
|
+
after: [],
|
|
1104
|
+
lookupTables: {},
|
|
1105
|
+
mode: null,
|
|
1106
|
+
solAmount: null,
|
|
1107
|
+
computeUnits: 0
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* The leg that puts `maxQuoteIn` of the quote token in the buyer's quote account.
|
|
1112
|
+
*
|
|
1113
|
+
* `payWith` decides where it comes from. On a WSOL pool the choice makes no difference: the quote
|
|
1114
|
+
* token is SOL either way, so the builder wraps it.
|
|
1115
|
+
*/
|
|
1116
|
+
async function quoteLegIn(client, input) {
|
|
1117
|
+
const { venue, payer, maxQuoteIn, payWith } = input;
|
|
1118
|
+
if (venue.quoteMint === "So11111111111111111111111111111111111111112" || payWith === "quote") return await quoteLegFromWallet(venue, payer, maxQuoteIn);
|
|
1119
|
+
const route = await routeQuoteIn(client, providerOf(client, venue.quoteMint), {
|
|
1120
|
+
quoteMint: venue.quoteMint,
|
|
1121
|
+
amount: maxQuoteIn,
|
|
1122
|
+
user: payer.address
|
|
1123
|
+
});
|
|
1124
|
+
assertRouteIsSafe(route, {
|
|
1125
|
+
forbidden: [GABOX_PROGRAM_ID, ...input.gaboxAccounts],
|
|
1126
|
+
settlesIn: venue.userQuoteToken
|
|
1127
|
+
});
|
|
1128
|
+
return {
|
|
1129
|
+
before: route.instructions,
|
|
1130
|
+
after: [],
|
|
1131
|
+
lookupTables: route.lookupTables,
|
|
1132
|
+
mode: route.mode,
|
|
1133
|
+
solAmount: route.inAmount,
|
|
1134
|
+
computeUnits: route.computeUnits
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* The leg around a sale: make sure the quote account exists, and turn the proceeds into SOL when
|
|
1139
|
+
* the seller asked for SOL.
|
|
1140
|
+
*
|
|
1141
|
+
* The swap is an exact-in of `minQuoteOutput`, the floor the seller already signs for on the sale
|
|
1142
|
+
* itself. Anything the venue pays above that floor stays in the seller's quote account: a swap can
|
|
1143
|
+
* only spend what the sale is guaranteed to have produced.
|
|
1144
|
+
*/
|
|
1145
|
+
async function quoteLegOut(client, input) {
|
|
1146
|
+
const { venue, seller, minQuoteOutput, receive } = input;
|
|
1147
|
+
if (venue.quoteMint === "So11111111111111111111111111111111111111112") {
|
|
1148
|
+
const wsol = await fundWsol(seller, 0n);
|
|
1149
|
+
return {
|
|
1150
|
+
before: wsol.instructions,
|
|
1151
|
+
after: [unwrapWsol(seller, wsol.account)],
|
|
1152
|
+
lookupTables: {},
|
|
1153
|
+
mode: null,
|
|
1154
|
+
solAmount: null,
|
|
1155
|
+
computeUnits: 0
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const create = createQuoteAccount(seller, venue);
|
|
1159
|
+
if (receive === "quote") return {
|
|
1160
|
+
before: [create],
|
|
1161
|
+
after: [],
|
|
1162
|
+
lookupTables: {},
|
|
1163
|
+
mode: null,
|
|
1164
|
+
solAmount: null,
|
|
1165
|
+
computeUnits: 0
|
|
1166
|
+
};
|
|
1167
|
+
const route = await routeQuoteOut(client, providerOf(client, venue.quoteMint), {
|
|
1168
|
+
quoteMint: venue.quoteMint,
|
|
1169
|
+
amount: minQuoteOutput,
|
|
1170
|
+
user: seller.address
|
|
1171
|
+
});
|
|
1172
|
+
assertRouteIsSafe(route, {
|
|
1173
|
+
forbidden: [GABOX_PROGRAM_ID, ...input.gaboxAccounts],
|
|
1174
|
+
settlesIn: venue.userQuoteToken
|
|
1175
|
+
});
|
|
1176
|
+
return {
|
|
1177
|
+
before: [create],
|
|
1178
|
+
after: route.instructions,
|
|
1179
|
+
lookupTables: route.lookupTables,
|
|
1180
|
+
mode: route.mode,
|
|
1181
|
+
solAmount: route.outAmount,
|
|
1182
|
+
computeUnits: route.computeUnits
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/** The client's route provider, with a message that says what to do when it has none. */
|
|
1186
|
+
function providerOf(client, quoteMint) {
|
|
1187
|
+
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.`);
|
|
1188
|
+
return client.route;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The compute limit a builder asks for: its own budget plus whatever the route needs.
|
|
1192
|
+
*
|
|
1193
|
+
* Capped at the runtime's ceiling. Jupiter often asks for the whole 1,400,000 units rather than
|
|
1194
|
+
* estimating, and a request above the ceiling is rejected outright, so the sum has to be clamped
|
|
1195
|
+
* rather than passed through.
|
|
1196
|
+
*/
|
|
1197
|
+
function computeUnitsWithRoute(own, leg) {
|
|
1198
|
+
return Math.min(own + leg.computeUnits, MAX_COMPUTE_UNIT_LIMIT);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Add the route to a "transaction is too large" error.
|
|
1202
|
+
*
|
|
1203
|
+
* `buildMessage` already refuses a message above the 1,232-byte limit. When a swap is in the same
|
|
1204
|
+
* message, the reason is usually the swap, and the fix is not to split the transaction: the two
|
|
1205
|
+
* halves have to settle together. So the message says what a caller can actually do instead.
|
|
1206
|
+
*/
|
|
1207
|
+
function routeSizeHint(cause, leg) {
|
|
1208
|
+
if (leg.mode === null) return cause;
|
|
1209
|
+
if (!(cause instanceof Error) || !cause.message.includes("Solana allows 1232")) return cause;
|
|
1210
|
+
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 });
|
|
1211
|
+
}
|
|
1212
|
+
//#endregion
|
|
925
1213
|
//#region src/tx/buyPack.ts
|
|
926
1214
|
async function buyPack(client, input) {
|
|
927
1215
|
if (input.maxQuoteIn <= 0n) throw new Error("maxQuoteIn must be positive");
|
|
@@ -933,6 +1221,11 @@ async function buyPack(client, input) {
|
|
|
933
1221
|
const venue = await resolveVenue(client, {
|
|
934
1222
|
mint: input.mint,
|
|
935
1223
|
user: purchaser,
|
|
1224
|
+
quote: {
|
|
1225
|
+
mint: pool.quoteMint,
|
|
1226
|
+
config: pool.quoteConfig,
|
|
1227
|
+
tokenProgram: pool.quoteTokenProgram
|
|
1228
|
+
},
|
|
936
1229
|
...input.venue ? { venue: input.venue } : {}
|
|
937
1230
|
});
|
|
938
1231
|
const draw = await drawAddress(poolAddress, input.seq ?? pool.nextSeq);
|
|
@@ -944,30 +1237,50 @@ async function buyPack(client, input) {
|
|
|
944
1237
|
quoteMint: pool.quoteMint,
|
|
945
1238
|
vault: pool.vault,
|
|
946
1239
|
venue: venue.program,
|
|
1240
|
+
quoteTokenProgram: pool.quoteTokenProgram,
|
|
947
1241
|
maxQuoteIn: input.maxQuoteIn,
|
|
948
1242
|
minMaximum: input.minMaximum,
|
|
949
1243
|
maxNativeDebit: input.maxNativeDebit
|
|
950
1244
|
});
|
|
951
|
-
const
|
|
1245
|
+
const leg = await quoteLegIn(client, {
|
|
1246
|
+
venue,
|
|
1247
|
+
payer: input.purchaser,
|
|
1248
|
+
maxQuoteIn: input.maxQuoteIn,
|
|
1249
|
+
payWith: input.payWith ?? "sol",
|
|
1250
|
+
gaboxAccounts: [
|
|
1251
|
+
poolAddress,
|
|
1252
|
+
pool.vault,
|
|
1253
|
+
draw,
|
|
1254
|
+
await activityAddress(purchaser),
|
|
1255
|
+
await associatedTokenAddress(purchaser, input.mint)
|
|
1256
|
+
]
|
|
1257
|
+
});
|
|
952
1258
|
const instructions = [
|
|
953
|
-
...
|
|
1259
|
+
...leg.before,
|
|
954
1260
|
withRemainingAccounts(buy, venue.buyAccounts),
|
|
955
|
-
|
|
1261
|
+
...leg.after
|
|
956
1262
|
];
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1263
|
+
try {
|
|
1264
|
+
return await buildMessage(client, input.purchaser, instructions, {
|
|
1265
|
+
addressLookupTables: {
|
|
1266
|
+
...input.addressLookupTables ?? client.addressLookupTables,
|
|
1267
|
+
...leg.lookupTables
|
|
1268
|
+
},
|
|
1269
|
+
computeUnitLimit: input.computeUnitLimit ?? computeUnitsWithRoute(235e3, leg),
|
|
1270
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1271
|
+
});
|
|
1272
|
+
} catch (cause) {
|
|
1273
|
+
throw routeSizeHint(cause, leg);
|
|
1274
|
+
}
|
|
962
1275
|
}
|
|
963
1276
|
//#endregion
|
|
964
1277
|
//#region src/tx/createMachine.ts
|
|
965
1278
|
/**
|
|
966
1279
|
* Build the transaction message. Sign it with both `creator` and `mintKeypair`.
|
|
967
1280
|
*
|
|
968
|
-
* Reads
|
|
969
|
-
*
|
|
970
|
-
* account is a derivation.
|
|
1281
|
+
* Reads the quote's LaunchLab config, the quote mint, and the Gabox platform config, because the
|
|
1282
|
+
* seed price and every quote-side account depend on them. Nothing else needs the chain: the coin
|
|
1283
|
+
* does not exist yet, so every other account is a derivation.
|
|
971
1284
|
*/
|
|
972
1285
|
async function createMachine(client, input) {
|
|
973
1286
|
const { creator, mintKeypair, name, symbol, uri, maxSeedQuoteIn, maxSeedNativeDebit } = input;
|
|
@@ -983,52 +1296,65 @@ async function createMachine(client, input) {
|
|
|
983
1296
|
if (maxSeedNativeDebit < 0n) throw new Error("maxSeedNativeDebit must not be negative");
|
|
984
1297
|
const ids = raydiumIds(client.cluster);
|
|
985
1298
|
const mint = mintKeypair.address;
|
|
986
|
-
const
|
|
1299
|
+
const quote = await fetchQuoteAsset(client, input.quote?.mint ?? "So11111111111111111111111111111111111111112", ids);
|
|
1300
|
+
const raise = resolveRaise(client, quote, input.raise, ids);
|
|
987
1301
|
const create = await getLaunchInstruction({
|
|
988
1302
|
mint: mintKeypair,
|
|
989
1303
|
creator,
|
|
990
1304
|
name,
|
|
991
1305
|
symbol,
|
|
992
|
-
uri
|
|
1306
|
+
uri,
|
|
1307
|
+
quoteMint: quote.mint,
|
|
1308
|
+
quoteConfig: quote.config,
|
|
1309
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1310
|
+
raise
|
|
993
1311
|
}, ids);
|
|
994
|
-
const poolState = await launchlabPoolAddress(ids.launchlab, mint,
|
|
1312
|
+
const poolState = await launchlabPoolAddress(ids.launchlab, mint, quote.mint);
|
|
1313
|
+
const userQuoteToken = await quoteAccountFor(creator.address, quote.mint, quote.tokenProgram);
|
|
995
1314
|
const venueAccounts = launchlabBuyAccounts({
|
|
996
1315
|
launchlab: ids.launchlab,
|
|
997
1316
|
launchlabAuthority: ids.launchlabAuthority,
|
|
998
1317
|
launchlabEventAuthority: ids.launchlabEventAuthority,
|
|
999
|
-
globalConfig:
|
|
1318
|
+
globalConfig: quote.config,
|
|
1000
1319
|
platformConfig: ids.gaboxPlatform,
|
|
1001
1320
|
poolState,
|
|
1002
1321
|
mint,
|
|
1003
|
-
quoteMint,
|
|
1322
|
+
quoteMint: quote.mint,
|
|
1004
1323
|
baseVault: await launchlabVaultAddress(ids.launchlab, poolState, mint),
|
|
1005
|
-
quoteVault: await launchlabVaultAddress(ids.launchlab, poolState,
|
|
1324
|
+
quoteVault: await launchlabVaultAddress(ids.launchlab, poolState, quote.mint),
|
|
1006
1325
|
user: creator.address,
|
|
1007
1326
|
userBaseToken: await ata(creator.address, mint),
|
|
1008
|
-
userQuoteToken
|
|
1009
|
-
|
|
1010
|
-
|
|
1327
|
+
userQuoteToken,
|
|
1328
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1329
|
+
platformFeeVault: await platformFeeVaultAddress(ids.launchlab, ids.gaboxPlatform, quote.mint),
|
|
1330
|
+
creatorFeeVault: await creatorFeeVaultAddress(ids.launchlab, creator.address, quote.mint)
|
|
1011
1331
|
});
|
|
1012
1332
|
const initialize = await getInitializePoolInstructionAsync({
|
|
1013
1333
|
creator,
|
|
1014
1334
|
mint,
|
|
1015
|
-
quoteMint,
|
|
1335
|
+
quoteMint: quote.mint,
|
|
1336
|
+
quoteConfig: quote.config,
|
|
1337
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1016
1338
|
venue: ids.launchlab,
|
|
1017
1339
|
tiers,
|
|
1018
1340
|
maxSeedQuoteIn,
|
|
1019
1341
|
maxSeedNativeDebit,
|
|
1020
1342
|
extraSeedTokens
|
|
1021
1343
|
});
|
|
1022
|
-
const
|
|
1344
|
+
const leg = await quoteLegFromWallet({
|
|
1345
|
+
quoteMint: quote.mint,
|
|
1346
|
+
quoteTokenProgram: quote.tokenProgram,
|
|
1347
|
+
userQuoteToken
|
|
1348
|
+
}, creator, maxSeedQuoteIn);
|
|
1023
1349
|
const instructions = [
|
|
1024
1350
|
create,
|
|
1025
|
-
...
|
|
1351
|
+
...leg.before,
|
|
1026
1352
|
withRemainingAccounts(initialize, venueAccounts),
|
|
1027
|
-
|
|
1353
|
+
...leg.after
|
|
1028
1354
|
];
|
|
1029
1355
|
return await buildMessage(client, creator, instructions, {
|
|
1030
|
-
addressLookupTables: input.addressLookupTables,
|
|
1031
|
-
computeUnitLimit: input.computeUnitLimit ??
|
|
1356
|
+
addressLookupTables: input.addressLookupTables ?? client.addressLookupTables,
|
|
1357
|
+
computeUnitLimit: input.computeUnitLimit ?? 35e4,
|
|
1032
1358
|
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1033
1359
|
});
|
|
1034
1360
|
}
|
|
@@ -1036,13 +1362,17 @@ async function createMachine(client, input) {
|
|
|
1036
1362
|
* What the seed for this table costs, fees included, and how many tokens it is.
|
|
1037
1363
|
*
|
|
1038
1364
|
* The coin does not exist yet, so the price comes from the starting reserves LaunchLab derives from
|
|
1039
|
-
* the
|
|
1040
|
-
* transaction, so this is exact up to a change in Raydium's fee rates between the read and the
|
|
1365
|
+
* the launch shape and the raise. Nothing trades on the curve before `initialize_pool` runs in the
|
|
1366
|
+
* same transaction, so this is exact up to a change in Raydium's fee rates between the read and the
|
|
1041
1367
|
* send.
|
|
1042
1368
|
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1369
|
+
* `solAmount` is what that cost is worth in SOL, priced through the client's route provider. It is
|
|
1370
|
+
* a display: the creator pays in the quote token. It is `null` when the client has no provider, or
|
|
1371
|
+
* when no route exists: a devnet client has none unless the caller passes `raydiumCpmmRoute(pool)`.
|
|
1372
|
+
*
|
|
1373
|
+
* Defaults to `DEFAULT_TIERS`, wrapped SOL and no extra seed. Throws if `tiers` fails
|
|
1374
|
+
* `validateTiers`/`validatePack`, if `extraSeedTokens` is negative, if the total seed is bigger
|
|
1375
|
+
* than the curve sells, or if the raise is missing or below what LaunchLab accepts.
|
|
1046
1376
|
*/
|
|
1047
1377
|
async function seedCostEstimate(client, tiers = DEFAULT_TIERS, options = {}) {
|
|
1048
1378
|
const copiedTiers = cloneTiers(tiers);
|
|
@@ -1054,16 +1384,39 @@ async function seedCostEstimate(client, tiers = DEFAULT_TIERS, options = {}) {
|
|
|
1054
1384
|
const seed = mandatorySeed + extraSeedTokens;
|
|
1055
1385
|
assertSeedFitsCurve(seed);
|
|
1056
1386
|
const ids = raydiumIds(client.cluster);
|
|
1057
|
-
const
|
|
1387
|
+
const quote = await fetchQuoteAsset(client, options.quote?.mint ?? "So11111111111111111111111111111111111111112", ids);
|
|
1388
|
+
const raise = resolveRaise(client, quote, options.raise, ids);
|
|
1389
|
+
const settings = await fetchCurveSettings(client, quote.config, ids);
|
|
1390
|
+
const quoteAmount = seed === 0n ? 0n : curveBuyExactOut(newCurveReserves(raise, settings.migrateFee), settings.rates, seed);
|
|
1058
1391
|
return {
|
|
1059
1392
|
tiers: copiedTiers,
|
|
1060
1393
|
seedTokens: mandatorySeed,
|
|
1061
1394
|
extraSeedTokens,
|
|
1062
1395
|
totalSeedTokens: seed,
|
|
1063
|
-
quoteAmount
|
|
1064
|
-
quoteMint:
|
|
1396
|
+
quoteAmount,
|
|
1397
|
+
quoteMint: quote.mint,
|
|
1398
|
+
quoteDecimals: quote.decimals,
|
|
1399
|
+
quoteSymbol: quote.symbol,
|
|
1400
|
+
raise,
|
|
1401
|
+
solAmount: await solPriceOf(client, quote.mint, quoteAmount)
|
|
1065
1402
|
};
|
|
1066
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* The raise a launch uses, checked before anything is built.
|
|
1406
|
+
*
|
|
1407
|
+
* The program pins it for a WSOL pool, so a different value there is a launch that would be
|
|
1408
|
+
* refused on chain. Any other quote has no default and no pin: the caller names it, and LaunchLab's
|
|
1409
|
+
* own minimum for that quote is the floor.
|
|
1410
|
+
*/
|
|
1411
|
+
function resolveRaise(client, quote, raise, ids) {
|
|
1412
|
+
if (quote.mint === "So11111111111111111111111111111111111111112") {
|
|
1413
|
+
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.`);
|
|
1414
|
+
return ids.launchQuoteRaise;
|
|
1415
|
+
}
|
|
1416
|
+
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.`);
|
|
1417
|
+
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}`);
|
|
1418
|
+
return raise;
|
|
1419
|
+
}
|
|
1067
1420
|
/** A mutable table shape, owned by this call and safe to pass to Codama's builder. */
|
|
1068
1421
|
function cloneTiers(tiers) {
|
|
1069
1422
|
return tiers.map(({ multiplierBps, tickets }) => ({
|
|
@@ -1162,29 +1515,53 @@ async function sellTokens(client, input) {
|
|
|
1162
1515
|
const venue = await resolveVenue(client, {
|
|
1163
1516
|
mint: input.mint,
|
|
1164
1517
|
user: input.seller.address,
|
|
1518
|
+
quote: {
|
|
1519
|
+
mint: pool.quoteMint,
|
|
1520
|
+
config: pool.quoteConfig,
|
|
1521
|
+
tokenProgram: pool.quoteTokenProgram
|
|
1522
|
+
},
|
|
1165
1523
|
...input.venue ? { venue: input.venue } : {}
|
|
1166
1524
|
});
|
|
1525
|
+
const gaboxPool = await poolAddress(input.mint);
|
|
1167
1526
|
const sell = await getSellTokensInstructionAsync({
|
|
1168
1527
|
seller: input.seller,
|
|
1169
|
-
pool:
|
|
1528
|
+
pool: gaboxPool,
|
|
1170
1529
|
mint: input.mint,
|
|
1171
1530
|
quoteMint: pool.quoteMint,
|
|
1172
1531
|
venue: venue.program,
|
|
1532
|
+
quoteTokenProgram: pool.quoteTokenProgram,
|
|
1173
1533
|
amount: input.amount,
|
|
1174
1534
|
minQuoteOutput: input.minQuoteOutput,
|
|
1175
1535
|
maxNativeDebit: input.maxNativeDebit
|
|
1176
1536
|
});
|
|
1177
|
-
const
|
|
1537
|
+
const leg = await quoteLegOut(client, {
|
|
1538
|
+
venue,
|
|
1539
|
+
seller: input.seller,
|
|
1540
|
+
minQuoteOutput: input.minQuoteOutput,
|
|
1541
|
+
receive: input.receive ?? "sol",
|
|
1542
|
+
gaboxAccounts: [
|
|
1543
|
+
gaboxPool,
|
|
1544
|
+
await vaultAddress(input.mint),
|
|
1545
|
+
await associatedTokenAddress(input.seller.address, input.mint)
|
|
1546
|
+
]
|
|
1547
|
+
});
|
|
1178
1548
|
const instructions = [
|
|
1179
|
-
...
|
|
1549
|
+
...leg.before,
|
|
1180
1550
|
withRemainingAccounts(sell, venue.sellAccounts),
|
|
1181
|
-
|
|
1551
|
+
...leg.after
|
|
1182
1552
|
];
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1553
|
+
try {
|
|
1554
|
+
return await buildMessage(client, input.seller, instructions, {
|
|
1555
|
+
addressLookupTables: {
|
|
1556
|
+
...input.addressLookupTables ?? client.addressLookupTables,
|
|
1557
|
+
...leg.lookupTables
|
|
1558
|
+
},
|
|
1559
|
+
computeUnitLimit: input.computeUnitLimit ?? computeUnitsWithRoute(125e3, leg),
|
|
1560
|
+
...input.computeUnitPrice === void 0 ? {} : { computeUnitPrice: input.computeUnitPrice }
|
|
1561
|
+
});
|
|
1562
|
+
} catch (cause) {
|
|
1563
|
+
throw routeSizeHint(cause, leg);
|
|
1564
|
+
}
|
|
1188
1565
|
}
|
|
1189
1566
|
//#endregion
|
|
1190
1567
|
//#region src/vrf.ts
|
|
@@ -1204,6 +1581,6 @@ async function oracleAccounts() {
|
|
|
1204
1581
|
};
|
|
1205
1582
|
}
|
|
1206
1583
|
//#endregion
|
|
1207
|
-
export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, BPS, BUY_PACK_COMPUTE_UNITS, CLAIM_COMPUTE_UNITS, CLUSTER_ENDPOINTS, COMPUTE_BUDGET_PROGRAM_ADDRESS, CREATE_MACHINE_COMPUTE_UNITS, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_TIERS, DEVNET_ADDRESS_LOOKUP_TABLES, DEVNET_HTTP, DEVNET_LOOKUP_TABLE_ADDRESS, DEVNET_LOOKUP_TABLE_ADDRESSES, DEVNET_WS, DRAW_DISCRIMINATOR, DRAW_POOL_OFFSET, DRAW_PURCHASER_OFFSET, GABOX_PROGRAM_ID, GaboxMathError, IDENTITY_SEED, INSTRUCTIONS_SYSVAR, LAUNCH_DECIMALS, MAX_ATTEMPTS, MAX_COMPUTE_UNIT_LIMIT, MAX_MULTIPLIER_BPS, METAPLEX_PROGRAM_ADDRESS, MIN_SEED_MULTIPLIER_BPS, PACK_TOKENS, PLATFORM_ADMIN, POOL_CREATOR_OFFSET, POOL_DISCRIMINATOR, POOL_MINT_OFFSET, REDEEM_COMPUTE_UNITS, RETRY_SLOTS, SLOT_HASHES_SYSVAR, SYSTEM_PROGRAM_ADDRESS, TICKETS, TIERS, TIMEOUT_SLOTS, TOKEN_PROGRAM_ADDRESS, VRF_DEFAULT_QUEUE, VRF_PROGRAM_ADDRESS, WALLET_ACTIVITY_DISCRIMINATOR, WSOL_MINT, activityAddress, assertClusterUrl, associatedTokenAddress, averageMultiplierBps, buildMessage, buyPack, choose, clusterNamedBy, computeBudgetInstructions, createClient, createMachine, decodeDraw, decodeEvent, decodeEvents, decodePool, decodeWalletActivity, defaultAddressLookupTables, drawAddress, drawAvailability, expireDraw, fetchDraw, fetchEvents, fetchPoolAt, fetchPoolByMint, fetchPoolInventory, fetchVaultBalance, fetchWalletActivity, findActivityPda, findDrawPda, findIdentityPda, findPoolPda, findResolvedDraw, fundPrizes, fundWsol, generated_exports as generated, getOffer, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, listDraws, listDrawsByPool, listDrawsByPurchaser, listPools, maxMultiplierBps, offerFromState, oracleAccounts, poolAddress, quote, quotePool, raydium_exports as raydium, resolveReservation, retryDraw, scopedVrfIdentityAddress, seedCostEstimate, seedShortfall, seedTokens, sellTokens, tierAmount, tiersOf, uncappedMaximum, unwrapWsol, validatePack, validateTiers, vaultAddress, vrfIdentityAddress, websocketUrlFor, withRemainingAccounts };
|
|
1584
|
+
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, quoteLegFromWallet, 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 };
|
|
1208
1585
|
|
|
1209
1586
|
//# sourceMappingURL=index.js.map
|