@xgamesfun/solana 0.25.4 → 0.26.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/README.md +8 -8
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +716 -183
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +210 -64
- package/js/economy/check.mjs +691 -261
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +68 -19
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +855 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +22 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +11 -2
- package/types/economy/cost.d.mts +1 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +31 -2
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/js/client/sizes.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Rent sizes from the current IDL. Fixed allocations reserve the largest enum and option forms,
|
|
2
2
|
// matching Anchor InitSpace. Dynamic records use their actual encoded fields.
|
|
3
|
+
import { addressBytes } from '../chain/base58.js';
|
|
3
4
|
import { encodeAccount, encodeBorsh } from '../chain/borsh.js';
|
|
5
|
+
import { uintLE, utf8 } from '../chain/bytes.js';
|
|
4
6
|
import IDL from './idl/loot.js';
|
|
7
|
+
import { lootAddress } from './pda.js';
|
|
5
8
|
|
|
6
9
|
const scalar = { bool: 1, u8: 1, u16: 2, u32: 4, i32: 4, u64: 8, i64: 8, u128: 16, pubkey: 32 };
|
|
7
10
|
// What a fixed-size account reserves for its lists and strings: a roll's fate (`Fate.shifts`, each key at most 32 bytes).
|
|
@@ -170,7 +173,7 @@ export function orderBytes(lootBox, shares = 0, opens = 1, items) {
|
|
|
170
173
|
|
|
171
174
|
// What one oracle answer's roll spends, counted at no less than each part costs (`order.rs`'s `RollCost`, measured by
|
|
172
175
|
// the program's `tests/roll_costs.rs` and `tests/roll_heap.rs`), against the compute and the heap the oracle gives its
|
|
173
|
-
// answer, less a margin.
|
|
176
|
+
// answer, less a margin. Every number of the program's count is here, and only here: a change of one is one edit.
|
|
174
177
|
const ROLL_BUDGET = 300_000n;
|
|
175
178
|
const ROLL_HEAP = 32n * 1024n - 512n;
|
|
176
179
|
const COST = {
|
|
@@ -184,7 +187,14 @@ const COST = {
|
|
|
184
187
|
tokens: 6_000n,
|
|
185
188
|
draw: 9_000n,
|
|
186
189
|
group: 250n,
|
|
190
|
+
row: 10n,
|
|
191
|
+
// One try of an address search, what the runtime charges (`COST_TRY`).
|
|
192
|
+
try: 1_500n,
|
|
193
|
+
// Reading a prize of an entry after its first (`COST_PRIZE`).
|
|
194
|
+
prize: 300n,
|
|
187
195
|
reread: 1_000n,
|
|
196
|
+
// A draw of an entry that gives nothing, beyond its weighing (`COST_NOTHING`).
|
|
197
|
+
nothing: 1_100n,
|
|
188
198
|
finish: 15_000n,
|
|
189
199
|
won: 150n,
|
|
190
200
|
drawn: 50n,
|
|
@@ -195,6 +205,22 @@ const COST = {
|
|
|
195
205
|
slotEntry: 350n,
|
|
196
206
|
openSlot: 150n,
|
|
197
207
|
};
|
|
208
|
+
/** The most draws one `open_box` takes in all, its count times the box's draws: what one answer could weigh, each draw
|
|
209
|
+
* at its weighing at least (`ROLL_BUDGET / COST_WEIGH`, 300). More is refused before any price is added up
|
|
210
|
+
* (`PackTooLarge`). */
|
|
211
|
+
export const MOST_DRAWS_A_PURCHASE = Number(ROLL_BUDGET / COST.weigh);
|
|
212
|
+
/** The compute one transaction has, and what adding up one sale's price costs at most (`catalog.rs` `COST_SALE`). */
|
|
213
|
+
const MAX_TX_CU = 1_400_000;
|
|
214
|
+
const COST_SALE = 55;
|
|
215
|
+
/** The most sales one `buy` takes (`count`): what one transaction can price (`MAX_TX_CU / COST_SALE`, rounded down:
|
|
216
|
+
* 25,454). More is refused before any price is added up (`PackTooLarge`). */
|
|
217
|
+
export const MOST_SALES_A_BUY = Math.floor(MAX_TX_CU / COST_SALE);
|
|
218
|
+
/**
|
|
219
|
+
* The tries counted for finding a pool's or a stash's address when the project is not known yet (a spec checked before
|
|
220
|
+
* any wallet is named): 2, what an address takes on average (half are found at the first try, a quarter at the second,
|
|
221
|
+
* and so on). With the project known, the count is exact: `256 - bump` tries an address.
|
|
222
|
+
*/
|
|
223
|
+
export const ASSUMED_TRIES = 2;
|
|
198
224
|
const BYTES = {
|
|
199
225
|
start: 6_000n,
|
|
200
226
|
prizeRead: 600n,
|
|
@@ -244,6 +270,27 @@ function borshLength(type, value) {
|
|
|
244
270
|
|
|
245
271
|
const defined = name => ({ defined: { name } });
|
|
246
272
|
|
|
273
|
+
/** The heap reading a list of `n` values of `size` bytes takes (`order::list_bytes`): Borsh makes room for as many as
|
|
274
|
+
* 4,096 bytes hold, then doubles it each time it fills, and every room it outgrew stays used beside the last. */
|
|
275
|
+
function listBytes(n, size) {
|
|
276
|
+
if (n * size <= 4096) return n * size;
|
|
277
|
+
let room = Math.min(Math.max(1, Math.floor(4096 / size)), n);
|
|
278
|
+
let rooms = room;
|
|
279
|
+
while (room < n) {
|
|
280
|
+
room *= 2;
|
|
281
|
+
rooms += room;
|
|
282
|
+
}
|
|
283
|
+
return rooms * size;
|
|
284
|
+
}
|
|
285
|
+
/** What `listBytes` takes beyond the `n` values themselves (`order::outgrown`). */
|
|
286
|
+
const outgrown = (n, size) => listBytes(n, size) - n * size;
|
|
287
|
+
/** The bytes a program `String` and a `DropEntry` take in memory, and an address. */
|
|
288
|
+
const STRING_BYTES = 24;
|
|
289
|
+
const ENTRY_BYTES = 32;
|
|
290
|
+
const ADDRESS_BYTES = 32;
|
|
291
|
+
/** The heap a rule's "one of" list of texts takes, read whole. */
|
|
292
|
+
const textsBytes = rule => (rule?.test?.OneOf ? listBytes(rule.test.OneOf.texts.length, STRING_BYTES) : 0);
|
|
293
|
+
|
|
247
294
|
/**
|
|
248
295
|
* The heap reading a box takes beyond its entries (`order::box_bytes`): its odds, gate, shares, subject, slots and
|
|
249
296
|
* fates, read whole with it, by their bytes and their number. `box`: the box's args, as `upsertBoxIx` takes them.
|
|
@@ -261,12 +308,20 @@ export function boxBytes({ odds = [], gate = null, shares = [], subject = null,
|
|
|
261
308
|
borshLength({ option: defined('Rule') }, subject) +
|
|
262
309
|
borshLength({ vec: defined('Slot') }, slotsWritten) +
|
|
263
310
|
borshLength({ vec: defined('Fate') }, fatesWritten);
|
|
311
|
+
// Each "one of" list of texts (a gate's rule, each odds rule, the subject), read whole; a gate's wallets' room past
|
|
312
|
+
// what they fill.
|
|
313
|
+
const rules =
|
|
314
|
+
(gate?.Holds ? textsBytes(gate.Holds[0]) : 0) +
|
|
315
|
+
odds.reduce((n, o) => n + textsBytes(o.boost?.rule), 0) +
|
|
316
|
+
textsBytes(subject) +
|
|
317
|
+
(gate?.Wallets ? outgrown(gate.Wallets[0].length, ADDRESS_BYTES) : 0);
|
|
264
318
|
return (
|
|
265
319
|
BYTES.boxByte * BigInt(bytes) +
|
|
266
320
|
BYTES.gateByte * BigInt(borshLength({ option: defined('Gate') }, gate)) +
|
|
267
321
|
BYTES.odds * BigInt(odds.length) +
|
|
268
322
|
BYTES.fate * BigInt(fates.length) +
|
|
269
|
-
BYTES.slot * BigInt(slots.length)
|
|
323
|
+
BYTES.slot * BigInt(slots.length) +
|
|
324
|
+
BigInt(rules)
|
|
270
325
|
);
|
|
271
326
|
}
|
|
272
327
|
|
|
@@ -281,85 +336,152 @@ const accountOf = p => {
|
|
|
281
336
|
};
|
|
282
337
|
|
|
283
338
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box takes beyond its entries. `slots` may
|
|
287
|
-
* be a number: that many draws among every entry. The answer's heap counts for a box that rolls, or one sold through
|
|
288
|
-
* the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry, `buyFits`). The program refuses a box whose
|
|
289
|
-
* one open doesn't fit at its upsert, and a purchase that doesn't at `open_box`, where it also reads what the accounts
|
|
290
|
-
* hold (`PackTooLarge`).
|
|
339
|
+
* The tries finding a pool's (`pool` true) or a stash's address takes (`order::searched`: `256 - bump`), for `project`'s
|
|
340
|
+
* pool or stash `id`. With no project, `ASSUMED_TRIES`.
|
|
291
341
|
*/
|
|
292
|
-
export function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
342
|
+
export function searchTries(project, pool, id) {
|
|
343
|
+
if (!project) return ASSUMED_TRIES;
|
|
344
|
+
const [, bump] = pool ? lootAddress(pdaSeeds.pool(project, id)) : lootAddress(pdaSeeds.stash(project, id));
|
|
345
|
+
return 256 - bump;
|
|
346
|
+
}
|
|
347
|
+
const pdaSeeds = {
|
|
348
|
+
pool: (project, index) => [utf8('pool'), addressBytes(project), uintLE(index, 2)],
|
|
349
|
+
stash: (project, id) => [utf8('stash'), addressBytes(project), uintLE(id, 4)],
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* What a roll of `entries` spends, as the program's `RollCost::new` counts it from the box alone (no table rows, a
|
|
354
|
+
* stash of one group, a named NFT's stash of one row): `fixed` (reading the box, every entry's accounts, finding each
|
|
355
|
+
* pool's and stash's address), each entry's draw (`prizes`), its stock read again (`reread`), the prizes it gives, its
|
|
356
|
+
* heap, and the heap reading the box leaves used (`start`, the box's own `bytes` included). `project`: whose pools and
|
|
357
|
+
* stashes these are, to count each search exactly; without it, at `ASSUMED_TRIES` tries an address.
|
|
358
|
+
*/
|
|
359
|
+
function rollCost(entries, { odds = 0, shares = 0, fates = 0, bytes = 0n, project = null }) {
|
|
301
360
|
const n = BigInt(entries.length);
|
|
361
|
+
// Each address is found by what finding it takes, once a prize names it (`draws_fit`'s `reads`): what a search cost is
|
|
362
|
+
// kept by the address's kind and id; once past the answer's budget nothing more is searched for (the box is refused).
|
|
363
|
+
const found = new Map();
|
|
364
|
+
let searched = 0n;
|
|
365
|
+
const reading = p => {
|
|
366
|
+
const pool = p.Tokens?.from?.Pool;
|
|
367
|
+
const stash = p.Stash ?? p.Nft?.stash;
|
|
368
|
+
if (pool == null && stash == null) return 0n;
|
|
369
|
+
const key = pool != null ? `pool ${pool[0] ?? pool}` : `stash ${stash[0] ?? stash}`;
|
|
370
|
+
let cost = found.get(key);
|
|
371
|
+
if (cost === undefined) {
|
|
372
|
+
if (searched > ROLL_BUDGET) cost = 0n;
|
|
373
|
+
else {
|
|
374
|
+
const id = Number(pool != null ? (pool[0] ?? pool) : (stash[0] ?? stash));
|
|
375
|
+
cost = COST.try * BigInt(searchTries(project, pool != null, id));
|
|
376
|
+
found.set(key, cost);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
searched += cost;
|
|
380
|
+
return cost;
|
|
381
|
+
};
|
|
382
|
+
let fixed = COST.fixed + COST.odds * BigInt(odds) + COST.share * BigInt(shares) + COST.fate * BigInt(fates);
|
|
383
|
+
let start = BYTES.start + BigInt(outgrown(entries.length, ENTRY_BYTES)) + BigInt(bytes);
|
|
384
|
+
const prizes = [];
|
|
385
|
+
const reread = [];
|
|
386
|
+
const heap = [];
|
|
387
|
+
for (const e of entries) {
|
|
388
|
+
let [set, again, used] = [0n, 0n, BYTES.draw];
|
|
389
|
+
fixed += e.prizes.length ? COST.entry : COST.blank;
|
|
390
|
+
start += BYTES.entryRead;
|
|
391
|
+
e.prizes.forEach((p, k) => {
|
|
392
|
+
fixed += reading(p) + (k > 0 ? COST.prize : 0n);
|
|
393
|
+
start += BYTES.prizeRead;
|
|
394
|
+
if (p.Item != null || p.Compressed) [set, again] = [set + COST.item, again + COST.reread];
|
|
395
|
+
else if (p.Tokens) [set, again] = [set + COST.tokens, again + (p.Tokens.from.Pool ? COST.reread : 0n)];
|
|
396
|
+
else if (p.Stash != null) [set, again] = [set + COST.draw + COST.group, again + COST.reread];
|
|
397
|
+
else if (p.Nft) {
|
|
398
|
+
// A named NFT is searched for row by row: when its stock is read, again, and when it is taken (one row here).
|
|
399
|
+
fixed += COST.row;
|
|
400
|
+
[set, again] = [set + COST.item + COST.row, again + COST.reread + COST.row];
|
|
401
|
+
} else set += COST.item;
|
|
402
|
+
used += p.Tokens ? (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted) : BYTES.prize;
|
|
403
|
+
});
|
|
404
|
+
// A draw that gives nothing still draws, and keeps its draw's lists.
|
|
405
|
+
prizes.push(e.prizes.length ? set : COST.nothing);
|
|
406
|
+
reread.push(again);
|
|
407
|
+
heap.push(used);
|
|
408
|
+
}
|
|
302
409
|
const weigh = COST.weigh + COST.weighEntry * n;
|
|
303
|
-
const reads = entries.map(e => e.prizes.map(accountOf));
|
|
304
|
-
const cost = entries.map(e =>
|
|
305
|
-
e.prizes.reduce(
|
|
306
|
-
(c, p) => {
|
|
307
|
-
if (p.Item != null || p.Compressed || p.Nft)
|
|
308
|
-
return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
309
|
-
if (p.Tokens)
|
|
310
|
-
return {
|
|
311
|
-
set: c.set + COST.tokens,
|
|
312
|
-
again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n),
|
|
313
|
-
bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted),
|
|
314
|
-
};
|
|
315
|
-
if (p.Stash != null)
|
|
316
|
-
return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
317
|
-
return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
|
|
318
|
-
},
|
|
319
|
-
{ set: 0n, again: 0n, bytes: BYTES.draw },
|
|
320
|
-
),
|
|
321
|
-
);
|
|
322
|
-
const next = entries.map(
|
|
323
|
-
(_, i) =>
|
|
324
|
-
weigh +
|
|
325
|
-
cost[i].set +
|
|
326
|
-
cost.reduce((sum, c, j) => (j === i || reads[i].some(a => a && reads[j].includes(a)) ? sum + c.again : sum), 0n),
|
|
327
|
-
);
|
|
328
410
|
const most = BigInt(Math.max(1, ...entries.map(e => e.prizes.length)));
|
|
411
|
+
return { fixed, start, weigh, prizes, reread, heap, knowing: COST.pair * n * n * most * most };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The entries whose stock a draw of entry `i` reads again: itself, and each naming an account it names. */
|
|
415
|
+
const sharing = (entries, i) => {
|
|
416
|
+
const reads = entries.map(e => e.prizes.map(accountOf));
|
|
417
|
+
return entries.map((_, j) => j).filter(j => j === i || reads[i].some(a => a && reads[j].includes(a)));
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Whether `opens` opens by `list` (slots, never empty) fit one answer (`RollCost::fits`): each draw at the dearest its
|
|
422
|
+
* slot could make, which entries share accounts found first when there are several draws. `next`: each entry's draw.
|
|
423
|
+
* `heap`: the answer's heap counts too.
|
|
424
|
+
*/
|
|
425
|
+
function fitsOne(cost, entries, list, opens, next, heap) {
|
|
426
|
+
const n = BigInt(entries.length);
|
|
427
|
+
const draws = list.reduce((d, s) => d + BigInt(s.count), 0n) * BigInt(opens);
|
|
329
428
|
let spent =
|
|
330
|
-
|
|
331
|
-
COST.
|
|
332
|
-
entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
|
|
333
|
-
let used =
|
|
334
|
-
BYTES.start +
|
|
335
|
-
BigInt(bytes) +
|
|
336
|
-
BYTES.entryRead * n +
|
|
337
|
-
BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
|
|
338
|
-
let won = 0n;
|
|
339
|
-
spent +=
|
|
340
|
-
(COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) +
|
|
429
|
+
cost.fixed +
|
|
430
|
+
(COST.slot + COST.slotEntry * n) * BigInt(list.length) +
|
|
341
431
|
COST.openSlot * BigInt(list.length) * BigInt(opens) +
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
432
|
+
(draws > 1n ? cost.knowing : 0n);
|
|
433
|
+
let used = cost.start;
|
|
434
|
+
let won = 0n;
|
|
345
435
|
for (const s of list) {
|
|
346
436
|
const count = BigInt(s.count) * BigInt(opens);
|
|
347
437
|
const among = entries.map((_, i) => i).filter(i => !(s.only ?? []).length || (s.only ?? []).includes(i));
|
|
348
438
|
const dearest = f => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
|
|
349
439
|
spent += count * dearest(i => next[i]);
|
|
350
|
-
used += count * dearest(i => cost[i]
|
|
440
|
+
used += count * dearest(i => cost.heap[i]);
|
|
351
441
|
won += count * dearest(i => BigInt(entries[i].prizes.length));
|
|
352
442
|
}
|
|
353
|
-
const rolls = oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
354
443
|
return (
|
|
355
444
|
spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET &&
|
|
356
|
-
(!
|
|
445
|
+
(!heap || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
|
|
357
446
|
);
|
|
358
447
|
}
|
|
359
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
|
|
451
|
+
* dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group, a named
|
|
452
|
+
* NFT's stash of one row), its `odds`, `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box
|
|
453
|
+
* takes beyond its entries. `slots` may be a number: that many draws among every entry. The answer's heap counts for a
|
|
454
|
+
* box that rolls, or one sold through the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry,
|
|
455
|
+
* `buyFits`). `project`: the project the box is in, whose pools' and stashes' addresses the write searches for (each
|
|
456
|
+
* search counted at `256 - bump` tries of 1,500 units); without it, at `ASSUMED_TRIES` tries an address. The program
|
|
457
|
+
* refuses a box whose one open doesn't fit at its upsert (`PackTooLarge`). `purchase`: counted as `open_box` counts a
|
|
458
|
+
* purchase of `opens` instead (the answer's heap always; a draw's own stock read again only when there are several; at
|
|
459
|
+
* most `MOST_DRAWS_A_PURCHASE` draws in all), from what the box alone tells: the program reads the accounts too.
|
|
460
|
+
*/
|
|
461
|
+
export function drawsFit(
|
|
462
|
+
entries,
|
|
463
|
+
slots,
|
|
464
|
+
opens = 1,
|
|
465
|
+
{ odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false, project = null, purchase = false } = {},
|
|
466
|
+
) {
|
|
467
|
+
const given = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
|
|
468
|
+
// A box with no slots draws once among every entry.
|
|
469
|
+
const list = given.length ? given : [{ count: 1, only: [] }];
|
|
470
|
+
const draws = list.reduce((d, s) => d + BigInt(s.count), 0n) * BigInt(opens);
|
|
471
|
+
if (purchase && draws > BigInt(MOST_DRAWS_A_PURCHASE)) return false;
|
|
472
|
+
const cost = rollCost(entries, { odds, shares, fates, bytes, project });
|
|
473
|
+
const heap = purchase || oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
474
|
+
// Reading the box alone already past the answer: refused before its entries are compared.
|
|
475
|
+
if (cost.fixed > ROLL_BUDGET || (heap && cost.start > ROLL_HEAP)) return false;
|
|
476
|
+
// Each entry's draw reads again the stock of the entries sharing its accounts; a purchase of one draw, its own alone.
|
|
477
|
+
const again = i => (purchase && draws <= 1n ? [] : sharing(entries, i)).reduce((s, j) => s + cost.reread[j], 0n);
|
|
478
|
+
const next = entries.map((_, i) => cost.weigh + cost.prizes[i] + again(i));
|
|
479
|
+
return fitsOne(cost, entries, list, opens, next, heap);
|
|
480
|
+
}
|
|
481
|
+
|
|
360
482
|
// What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
|
|
361
483
|
// accounts, 64 instructions and 1,400,000 compute units.
|
|
362
|
-
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu:
|
|
484
|
+
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: MAX_TX_CU } };
|
|
363
485
|
const sameJson = (a, b) =>
|
|
364
486
|
JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) ===
|
|
365
487
|
JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
|
|
@@ -412,6 +534,28 @@ export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s)
|
|
|
412
534
|
return accounts <= BUY.most.accounts && calls <= BUY.most.calls && cu <= BUY.most.cu;
|
|
413
535
|
}
|
|
414
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Whether the program writes `box` (its args, as `upsertBoxIx` takes them, or a box read from the chain) as far as what
|
|
539
|
+
* it costs goes (`catalog::loot_box`, `PackTooLarge`): a box that doesn't roll sells by `buy` when one transaction carries
|
|
540
|
+
* the whole purchase (`buyFits`), else through the oracle, whose heap then counts and which can't give a roll; and every
|
|
541
|
+
* draw of one open fits one answer (`drawsFit`). `project`: whose pools and stashes the box names (a box read from the
|
|
542
|
+
* chain says); without it, each search at `ASSUMED_TRIES` tries.
|
|
543
|
+
*/
|
|
544
|
+
export function boxFits(box, { project = box.project ?? null } = {}) {
|
|
545
|
+
const entries = box.entries ?? [];
|
|
546
|
+
const rolls = entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
547
|
+
const byBuy = rolls || box.subject != null || buyFits(box);
|
|
548
|
+
if (!byBuy && entries.some(e => e.prizes.some(p => p.Roll != null))) return false;
|
|
549
|
+
return drawsFit(entries, box.slots?.length ? box.slots : 1, 1, {
|
|
550
|
+
odds: (box.odds ?? []).length,
|
|
551
|
+
shares: (box.shares ?? []).length,
|
|
552
|
+
fates: (box.fates ?? []).length,
|
|
553
|
+
bytes: boxBytes(box),
|
|
554
|
+
oracle: !byBuy,
|
|
555
|
+
project,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
415
559
|
/** The most program rent and fee one order takes from its player, in lamports (0.01 SOL): a client cap (`opensAnOrder`). */
|
|
416
560
|
export const ORDER_COST_CAP = 10_000_000n;
|
|
417
561
|
/** Rent a byte, as devnet and mainnet-beta charge it ((bytes + 128) × 5,080 lamports). */
|
|
@@ -420,7 +564,8 @@ const RENT_PER_BYTE = 5_080n;
|
|
|
420
564
|
const PLAYER_FEE = 2_000_000n;
|
|
421
565
|
|
|
422
566
|
/**
|
|
423
|
-
* The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit`
|
|
567
|
+
* The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit` as a purchase counts
|
|
568
|
+
* it, the box's project's searches counted when the box names it: a box read from the chain does), and what keeps the
|
|
424
569
|
* order's rent and the player's fee within `ORDER_COST_CAP` (`orderBytes` with `items`, so with the box's shares). At
|
|
425
570
|
* least one: a box whose one open is over the cap is still bought one open an order. A purchase of more is several
|
|
426
571
|
* orders, each its own answer.
|
|
@@ -435,10 +580,11 @@ export function opensAnOrder(
|
|
|
435
580
|
shares,
|
|
436
581
|
fates: (lootBox.fates ?? []).length,
|
|
437
582
|
bytes: boxBytes(lootBox),
|
|
438
|
-
|
|
583
|
+
project: lootBox.project ?? null,
|
|
584
|
+
purchase: true,
|
|
439
585
|
}) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
|
|
440
586
|
let n = 1;
|
|
441
|
-
while (n <
|
|
587
|
+
while (n < MOST_DRAWS_A_PURCHASE && fits(n + 1)) n += 1;
|
|
442
588
|
return n;
|
|
443
589
|
}
|
|
444
590
|
|