@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.
Files changed (44) hide show
  1. package/README.md +8 -8
  2. package/js/approve/details.mjs +52 -2
  3. package/js/approve/live-check.mjs +0 -15
  4. package/js/approve/server.mjs +5 -4
  5. package/js/chain/token2022.js +3 -2
  6. package/js/client/accounts.js +67 -25
  7. package/js/client/grant.js +7 -2
  8. package/js/client/idl/loot.js +1 -1
  9. package/js/client/idl/loot.json +716 -183
  10. package/js/client/ix.js +617 -113
  11. package/js/client/sizes.js +210 -64
  12. package/js/economy/check.mjs +691 -261
  13. package/js/economy/cost.mjs +2 -4
  14. package/js/economy/explain.mjs +16 -2
  15. package/js/economy/guide.mjs +12 -12
  16. package/js/economy/model.mjs +133 -14
  17. package/js/economy/plan.mjs +68 -19
  18. package/js/economy/review.mjs +50 -8
  19. package/js/economy/schema.json +73 -29
  20. package/js/economy/simulate.mjs +116 -13
  21. package/js/mcp/tools.mjs +2 -2
  22. package/js/play/index.js +855 -296
  23. package/package.json +1 -1
  24. package/release.json +5 -5
  25. package/skills/xsolana-economy/SKILL.md +12 -4
  26. package/skills/xsolana-economy/references/after-launch.md +1 -1
  27. package/skills/xsolana-economy/references/apps.md +11 -2
  28. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  29. package/skills/xsolana-economy/references/telling-the-person.md +22 -4
  30. package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
  31. package/types/chain/token2022.d.ts +3 -2
  32. package/types/client/accounts.d.ts +34 -11
  33. package/types/client/idl/loot.d.ts +25 -4
  34. package/types/client/ix.d.ts +217 -45
  35. package/types/client/sizes.d.ts +42 -8
  36. package/types/economy/check.d.mts +11 -2
  37. package/types/economy/cost.d.mts +1 -0
  38. package/types/economy/live.d.mts +2 -0
  39. package/types/economy/model.d.mts +75 -12
  40. package/types/economy/plan.d.mts +31 -2
  41. package/types/economy/simulate.d.mts +12 -1
  42. package/types/mcp/server.d.mts +12 -1
  43. package/types/mcp/tools.d.mts +12 -1
  44. package/types/play/index.d.ts +210 -144
@@ -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
- * Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
285
- * dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group), its `odds`,
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 drawsFit(
293
- entries,
294
- slots,
295
- opens = 1,
296
- { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {},
297
- ) {
298
- const list = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
299
- const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
300
- if (draws <= 1n) return true;
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
- COST.fixed +
331
- COST.pair * n * n * most * most +
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
- COST.odds * BigInt(odds) +
343
- COST.share * BigInt(shares) +
344
- COST.fate * BigInt(fates);
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].bytes);
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
- (!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
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: 1_400_000 } };
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`), and what keeps the
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
- oracle: true,
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 < 4096 && fits(n + 1)) n += 1;
587
+ while (n < MOST_DRAWS_A_PURCHASE && fits(n + 1)) n += 1;
442
588
  return n;
443
589
  }
444
590