@xgamesfun/solana 0.26.0 → 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.
@@ -2,7 +2,7 @@
2
2
  "address": "9cwqM1TXytPnFZsLfmUyv2dg4uW6Ch9C6UJVPns7NFQ",
3
3
  "metadata": {
4
4
  "name": "loot",
5
- "version": "0.26.0",
5
+ "version": "0.26.1",
6
6
  "spec": "0.1.0",
7
7
  "description": "Economy for Anything: one program every coin of a project passes through",
8
8
  "repository": "https://github.com/gamesfun/loot"
@@ -9551,7 +9551,7 @@
9551
9551
  {
9552
9552
  "code": 6141,
9553
9553
  "name": "PackTooLarge",
9554
- "msg": "A box's draws (one, or each open's times the opens bought together) must all fit one answer of the oracle's: fewer entries, prizes an entry or draws"
9554
+ "msg": "A box's draws (one, or each open's times the opens bought together) must all fit one answer of the oracle's, and the sales one buy adds up one transaction: fewer entries, prizes an entry, draws or opens"
9555
9555
  },
9556
9556
  {
9557
9557
  "code": 6142,
@@ -11575,7 +11575,8 @@
11575
11575
  "docs": [
11576
11576
  "The carry: rounding of a share over the total weight, times `PRECISION`. The next move before",
11577
11577
  "the farm's end credits it to the seats there once it adds up to a share; a move that leaves no",
11578
- "weight, or any move once the farm is over, frees its whole tokens (`FarmSide::settle`)."
11578
+ "weight, or any move once the farm is over, frees its whole tokens (`FarmSide::settle`), and so",
11579
+ "does a walk that begins with no weight or where the weight reaches 0 (`Farm::walk`)."
11579
11580
  ],
11580
11581
  "type": "u128"
11581
11582
  },
@@ -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,
@@ -310,88 +336,152 @@ const accountOf = p => {
310
336
  };
311
337
 
312
338
  /**
313
- * Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
314
- * dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group), its `odds`,
315
- * `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box takes beyond its entries. `slots` may
316
- * be a number: that many draws among every entry. The answer's heap counts for a box that rolls, or one sold through
317
- * the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry, `buyFits`). The program refuses a box whose
318
- * one open doesn't fit at its upsert, and a purchase that doesn't at `open_box`, where it also reads what the accounts
319
- * 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`.
320
341
  */
321
- export function drawsFit(
322
- entries,
323
- slots,
324
- opens = 1,
325
- { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {},
326
- ) {
327
- const given = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
328
- // A box with no slots draws once among every entry.
329
- const list = given.length ? given : [{ count: 1, only: [] }];
330
- const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
331
- // A box of one draw is held to the same answer: one past it would be refused at every purchase.
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 }) {
332
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
+ }
333
409
  const weigh = COST.weigh + COST.weighEntry * n;
334
- const reads = entries.map(e => e.prizes.map(accountOf));
335
- const cost = entries.map(e =>
336
- e.prizes.reduce(
337
- (c, p) => {
338
- if (p.Item != null || p.Compressed || p.Nft)
339
- return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
340
- if (p.Tokens)
341
- return {
342
- set: c.set + COST.tokens,
343
- again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n),
344
- bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted),
345
- };
346
- if (p.Stash != null)
347
- return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
348
- return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
349
- },
350
- { set: 0n, again: 0n, bytes: BYTES.draw },
351
- ),
352
- );
353
- const next = entries.map(
354
- (_, i) =>
355
- weigh +
356
- cost[i].set +
357
- cost.reduce((sum, c, j) => (j === i || reads[i].some(a => a && reads[j].includes(a)) ? sum + c.again : sum), 0n),
358
- );
359
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);
360
428
  let spent =
361
- COST.fixed +
362
- COST.pair * n * n * most * most +
363
- entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
364
- let used =
365
- BYTES.start +
366
- BigInt(outgrown(entries.length, ENTRY_BYTES)) +
367
- BigInt(bytes) +
368
- BYTES.entryRead * n +
369
- BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
370
- let won = 0n;
371
- spent +=
372
- (COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) +
429
+ cost.fixed +
430
+ (COST.slot + COST.slotEntry * n) * BigInt(list.length) +
373
431
  COST.openSlot * BigInt(list.length) * BigInt(opens) +
374
- COST.odds * BigInt(odds) +
375
- COST.share * BigInt(shares) +
376
- COST.fate * BigInt(fates);
432
+ (draws > 1n ? cost.knowing : 0n);
433
+ let used = cost.start;
434
+ let won = 0n;
377
435
  for (const s of list) {
378
436
  const count = BigInt(s.count) * BigInt(opens);
379
437
  const among = entries.map((_, i) => i).filter(i => !(s.only ?? []).length || (s.only ?? []).includes(i));
380
438
  const dearest = f => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
381
439
  spent += count * dearest(i => next[i]);
382
- used += count * dearest(i => cost[i].bytes);
440
+ used += count * dearest(i => cost.heap[i]);
383
441
  won += count * dearest(i => BigInt(entries[i].prizes.length));
384
442
  }
385
- const rolls = oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
386
443
  return (
387
444
  spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET &&
388
- (!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
445
+ (!heap || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
389
446
  );
390
447
  }
391
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
+
392
482
  // What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
393
483
  // accounts, 64 instructions and 1,400,000 compute units.
394
- 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 } };
395
485
  const sameJson = (a, b) =>
396
486
  JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) ===
397
487
  JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
@@ -444,6 +534,28 @@ export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s)
444
534
  return accounts <= BUY.most.accounts && calls <= BUY.most.calls && cu <= BUY.most.cu;
445
535
  }
446
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
+
447
559
  /** The most program rent and fee one order takes from its player, in lamports (0.01 SOL): a client cap (`opensAnOrder`). */
448
560
  export const ORDER_COST_CAP = 10_000_000n;
449
561
  /** Rent a byte, as devnet and mainnet-beta charge it ((bytes + 128) × 5,080 lamports). */
@@ -452,7 +564,8 @@ const RENT_PER_BYTE = 5_080n;
452
564
  const PLAYER_FEE = 2_000_000n;
453
565
 
454
566
  /**
455
- * 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
456
569
  * order's rent and the player's fee within `ORDER_COST_CAP` (`orderBytes` with `items`, so with the box's shares). At
457
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
458
571
  * orders, each its own answer.
@@ -467,10 +580,11 @@ export function opensAnOrder(
467
580
  shares,
468
581
  fates: (lootBox.fates ?? []).length,
469
582
  bytes: boxBytes(lootBox),
470
- oracle: true,
583
+ project: lootBox.project ?? null,
584
+ purchase: true,
471
585
  }) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
472
586
  let n = 1;
473
- while (n < 4096 && fits(n + 1)) n += 1;
587
+ while (n < MOST_DRAWS_A_PURCHASE && fits(n + 1)) n += 1;
474
588
  return n;
475
589
  }
476
590
 
@@ -9,7 +9,7 @@ import { gateList } from '../client/grant.js';
9
9
  import { MAX_BID_NFTS, MAX_SHARES } from '../client/ix.js';
10
10
  import { MAX_POTS } from '../client/accounts.js';
11
11
  import { vault } from '../client/pda.js';
12
- import { boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
12
+ import { ASSUMED_TRIES, boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
13
13
  import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
14
14
  import { addressBytes, encodeBase58 } from '../chain/base58.js';
15
15
  import { isOnCurve } from '../chain/curve.js';
@@ -59,7 +59,7 @@ import {
59
59
  weights,
60
60
  } from './model.mjs';
61
61
  // plan.mjs imports check() in turn: each module only calls the other's functions, never at load time.
62
- import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepBytes } from './plan.mjs';
62
+ import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepAloneBytes } from './plan.mjs';
63
63
  import { findSecrets } from './secrets.mjs';
64
64
  import { simulate } from './simulate.mjs';
65
65
  import { BUDGET, MAX_TRANSACTION_BYTES, transactionLocks, transactionSize, WALLET_ROOM } from './txsize.mjs';
@@ -685,10 +685,16 @@ export function dateWords(seconds) {
685
685
  }
686
686
 
687
687
  /**
688
- * Checks an economy spec. `now` (Unix seconds) dates "publish".
688
+ * Checks an economy spec. `now` (Unix seconds) dates "publish". `project`: the project's address, once a wallet is named
689
+ * (the plan for a real setup passes it; a live economy's file names it): a box's write searches for each of its pools'
690
+ * and stashes' addresses, and the checker counts each search exactly with it, else at an assumed 2 tries an address
691
+ * (`ASSUMED_TRIES`).
689
692
  * Returns { ok, refusals: [{ code, where, message, fix }], warnings: [{ code, where, message, fix? }] }.
690
693
  */
691
- export function check(economy, { network = 'devnet', now = Math.floor(Date.now() / 1000) } = {}) {
694
+ export function check(
695
+ economy,
696
+ { network = 'devnet', now = Math.floor(Date.now() / 1000), project = economy?.live?.project ?? null } = {},
697
+ ) {
692
698
  const refusals = [];
693
699
  const warnings = [];
694
700
  const refuse = (code, where, message, fix) => refusals.push({ code, where, message, fix });
@@ -2398,7 +2404,9 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
2398
2404
  // rolls, or one outcome of items, with no outcome that burns the payment and no NFT to roll on (BadEntries).
2399
2405
  const slots = b.slots ?? [];
2400
2406
  const outs = b.outcomes ?? [];
2407
+ const heldBefore = refusals.length;
2401
2408
  answerHolds(b, where);
2409
+ const answerRefused = refusals.length > heldBefore;
2402
2410
  for (const [i, o] of outs.entries()) {
2403
2411
  const given = outcomeParts(o);
2404
2412
  tooMany(
@@ -2555,8 +2563,10 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
2555
2563
  // Every draw of one open fits one answer of the oracle's (PackTooLarge): a pack past it could only draw its first
2556
2564
  // cards, so the program refuses the box. A box that doesn't roll sells by `buy`, the whole purchase in one
2557
2565
  // transaction, or through the oracle when one `buy` can't carry it (`buyFits`): then the answer's heap counts, and it
2558
- // can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`).
2559
- if (cards > 1 && outs.length) {
2566
+ // can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`), and
2567
+ // the compute finding each pool's and stash's address its prizes name (exact with the project, else assumed).
2568
+ // A box of one draw is held to the same count: past it the program refuses the box when it is written.
2569
+ if (outs.length && (cards > 1 || !answerRefused)) {
2560
2570
  const args = (() => {
2561
2571
  try {
2562
2572
  return boxArgs(b, m);
@@ -2566,38 +2576,45 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
2566
2576
  })();
2567
2577
  const entries = args?.entries;
2568
2578
  if (entries) {
2569
- const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, cards);
2579
+ const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, Math.max(1, cards));
2570
2580
  const extras = {
2571
2581
  odds: args.odds.length,
2572
2582
  shares: args.shares.length,
2573
2583
  fates: args.fates.length,
2574
2584
  bytes: boxBytes(args),
2575
2585
  oracle: !byBuy,
2586
+ project,
2576
2587
  };
2588
+ const searches = entries.some(e => e.prizes.some(p => p.Tokens?.from?.Pool || p.Stash != null || p.Nft));
2589
+ const searched = searches
2590
+ ? project
2591
+ ? ' (finding the address of each pool and stash it names counted for this project)'
2592
+ : ` (finding the address of each pool and stash it names counted at ${ASSUMED_TRIES} tries each, as no wallet is named yet: the check with your wallet counts it exactly)`
2593
+ : '';
2594
+ const drawn = slots.length ? slots.map(s => ({ count: s.count, only: s.from ?? [] })) : 1;
2577
2595
  if (!byBuy && entries.some(e => e.prizes.some(p => p.Roll != null)))
2578
2596
  refuse(
2579
2597
  'E-ODDS',
2580
2598
  `${where}.slots`,
2581
- `${label(b)} gives a roll, which only a sale in one step gives, but ${cards} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
2599
+ `${label(b)} gives a roll, which only a sale in one step gives, but ${Math.max(1, cards)} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
2582
2600
  'Fewer cards an open, fewer prizes in its outcome, or fewer shares in its split.',
2583
2601
  );
2584
- else if (
2585
- !drawsFit(
2586
- entries,
2587
- slots.map(s => ({ count: s.count, only: s.from ?? [] })),
2588
- 1,
2589
- extras,
2590
- )
2591
- ) {
2602
+ else if (cards > 1 && !drawsFit(entries, drawn, 1, extras)) {
2592
2603
  let most = cards - 1;
2593
2604
  while (most > 1 && !drawsFit(entries, most, 1, extras)) most -= 1;
2594
2605
  refuse(
2595
2606
  'E-ODDS',
2596
2607
  `${where}.slots`,
2597
- `${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here), so the program refuses the box (PackTooLarge).`,
2608
+ `${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here${searched}), so the program refuses the box (PackTooLarge).`,
2598
2609
  'Fewer cards an open (sell two packs instead of one), fewer outcomes, fewer prizes in an outcome, or a smaller box (fewer odds, shares, slots or outcomes that act on the NFT).',
2599
2610
  );
2600
- }
2611
+ } else if (cards <= 1 && !drawsFit(entries, drawn, 1, extras))
2612
+ refuse(
2613
+ 'E-ODDS',
2614
+ `${where}.outcomes`,
2615
+ `${label(b)} is too long for one answer of the oracle’s to draw it once${searched}, so the program refuses the box when it is written (PackTooLarge).`,
2616
+ 'Fewer outcomes, fewer prizes in an outcome, fewer pools or stashes named, or a smaller box (fewer odds, shares or slots). Put a long weighted list in a table or a stash, drawn by one outcome.',
2617
+ );
2601
2618
  }
2602
2619
  }
2603
2620
  if (cards > 1 && b.rollsOn)
@@ -5700,7 +5717,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
5700
5717
  for (const step of setup?.steps ?? []) {
5701
5718
  let bytes;
5702
5719
  try {
5703
- bytes = stepBytes(step.build(ctx), ctx);
5720
+ bytes = stepAloneBytes(step, ctx);
5704
5721
  } catch {
5705
5722
  continue;
5706
5723
  }
@@ -39,7 +39,7 @@ export const PARTS = {
39
39
  boxes: {
40
40
  def: 'box',
41
41
  nested: ['outcome', 'odds', 'gate', 'rule', 'effect', 'rise', 'fall', 'split', 'perWalletEach'],
42
- says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each outcome gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), several of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. How long a box can be is what one answer of the oracle’s holds (measured: 128 outcomes that give nothing, 29 of token amounts, 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a gate): a longer weighted list goes in a table or a stash, drawn by one outcome. A roll is on its written odds whatever is in stock, and `whenOut` says what a draw on an outcome that is out does: "stopSales" (the default) stops selling while one couldn’t be covered, a paid miss gives that draw’s worth back at the settle (with no draw made a paid open fails and is refunded), and a free roll goes back to waiting and is asked again until it hits; "drawLeft" draws among what is left; "giveNothing" keeps selling and a miss gives nothing, paid or free. Odds rules (`odds`) add any percent (99,900 makes an outcome a thousand times likelier; a weight stops at the largest, 4,294,967,295) and count on every route, read when a roll is first asked. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
42
+ says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each outcome gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), several of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. How long a box can be is what one answer of the oracle’s holds (measured: 128 outcomes that give nothing, 29 of token amounts, 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a gate; fewer where its outcomes name pools or stashes, each search for one’s address counted: sixteen stashes fit 9 draws an open where finding them takes 36 tries): a longer weighted list goes in a table or a stash, drawn by one outcome. A roll is on its written odds whatever is in stock, and `whenOut` says what a draw on an outcome that is out does: "stopSales" (the default) stops selling while one couldn’t be covered, a paid miss gives that draw’s worth back at the settle (with no draw made a paid open fails and is refunded), and a free roll goes back to waiting and is asked again until it hits; "drawLeft" draws among what is left; "giveNothing" keeps selling and a miss gives nothing, paid or free. Odds rules (`odds`) add any percent (99,900 makes an outcome a thousand times likelier; a weight stops at the largest, 4,294,967,295) and count on every route, read when a roll is first asked. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
43
43
  acts: `the player opens (${sol(FEES.player)} SOL + ${sol(ORACLE_FEE)} SOL to the oracle); its player, a key they named or the project’s keys settle. Chance that pays money is flagged`,
44
44
  },
45
45
  payments: {
@@ -21,7 +21,12 @@ import {
21
21
  } from '../chain/pda.js';
22
22
  import { createTableIx, extendTableIx } from '../chain/lookup.js';
23
23
  import { MEMO_PROGRAM } from '../chain/token2022.js';
24
- import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
24
+ import {
25
+ computeUnitLimit,
26
+ createAssociatedTokenAccountIdempotent,
27
+ syncNative,
28
+ transferLamports,
29
+ } from '../chain/programs.js';
25
30
  import {
26
31
  createAccountIx,
27
32
  initializeMint2Ix,
@@ -34,7 +39,7 @@ import { compileMessage, transactionSize } from '../chain/tx.js';
34
39
  import { sha256 } from '../chain/sha256.js';
35
40
  import { utf8 } from '../chain/bytes.js';
36
41
  import * as loot from '../client/index.js';
37
- import { check } from './check.mjs';
42
+ import { check, MAX_COMPUTE_UNITS } from './check.mjs';
38
43
  import {
39
44
  ACCOUNT_OVERHEAD,
40
45
  boxBuys,
@@ -944,6 +949,10 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
944
949
  id: `${part}:${kind}`,
945
950
  part,
946
951
  says,
952
+ // A box's write finds each of its pools' and stashes' addresses (1,500 units a try) and compares its entries
953
+ // pair by pair: a box of 16 stashes takes some 170,000 units, more where its addresses take more tries, past
954
+ // the 200,000 an instruction has by default. Its transaction asks for all a transaction can have.
955
+ compute: Boolean(def.Box),
947
956
  creates: [{ what, bytes }, ...pot],
948
957
  build: ctx => [
949
958
  loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) }),
@@ -1785,7 +1794,18 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
1785
1794
  export const stepBytes = (instructions, ctx) =>
1786
1795
  transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
1787
1796
 
1788
- /** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
1797
+ /** `instructions`, led by a compute limit of all a transaction can have when `compute` (a box's write is in them). */
1798
+ const budgeted = (instructions, compute) =>
1799
+ compute ? [computeUnitLimit(MAX_COMPUTE_UNITS), ...instructions] : instructions;
1800
+
1801
+ /** The bytes of a transaction of `step` alone, as `pack` sends it (its compute limit included). */
1802
+ export const stepAloneBytes = (step, ctx) => stepBytes(budgeted(step.build(ctx), Boolean(step.compute)), ctx);
1803
+
1804
+ /**
1805
+ * Packs steps into transactions under the size budget, one signer set at a time; each step whole. A transaction that
1806
+ * carries a step needing more compute than an instruction has by default (`compute`: a box's write) asks for all a
1807
+ * transaction can have, first.
1808
+ */
1789
1809
  export function pack(steps, ctx) {
1790
1810
  const transactions = [];
1791
1811
  let current = null;
@@ -1795,25 +1815,28 @@ export function pack(steps, ctx) {
1795
1815
  // A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
1796
1816
  if (!instructions.length) continue;
1797
1817
  const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
1798
- // A write that names boxes to roll takes all the compute a transaction has: alone.
1799
1818
  if (current && current.signer === signerKind && !step.alone && !current.alone) {
1800
- const next = [...current.instructions, ...instructions];
1819
+ const compute = current.compute || Boolean(step.compute);
1820
+ const next = budgeted([...current.built.flat(), ...instructions], compute);
1801
1821
  if (sizeOf(next) <= BUDGET) {
1802
1822
  current.instructions = next;
1823
+ current.compute = compute;
1803
1824
  current.steps.push(step);
1804
1825
  current.built.push(instructions);
1805
1826
  current.signers = [...new Set([...current.signers, ...step.signers])];
1806
1827
  continue;
1807
1828
  }
1808
1829
  }
1809
- const size = sizeOf(instructions);
1830
+ const alone = budgeted(instructions, Boolean(step.compute));
1831
+ const size = sizeOf(alone);
1810
1832
  if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
1811
1833
  // `built`: each step's own instructions, in the order of `steps`.
1812
1834
  current = {
1813
1835
  signer: signerKind,
1814
1836
  steps: [step],
1815
1837
  built: [instructions],
1816
- instructions,
1838
+ instructions: alone,
1839
+ compute: Boolean(step.compute),
1817
1840
  signers: [...step.signers],
1818
1841
  alone: Boolean(step.alone),
1819
1842
  };
@@ -1828,7 +1851,8 @@ export function pack(steps, ctx) {
1828
1851
  * real instructions; without it they are built on stand-in addresses (for sizing, cost and showing).
1829
1852
  */
1830
1853
  export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null, only = null, live = null } = {}) {
1831
- const checked = check(economy, { now });
1854
+ // With a real context the project is known: a box's write is checked with its own address searches counted.
1855
+ const checked = check(economy, { now, ...(ctx?.project ? { project: ctx.project } : {}) });
1832
1856
  if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
1833
1857
  const { steps, model: m } = setupSteps(economy, { now, only, live });
1834
1858
  if (ctx)